虚拟线程专属死锁问题求助:仅50线程场景下触发
问题场景
使用虚拟线程池时,当CONCURRENT_WORKERS设为50,主线程调用completionService.take()会无限等待;设为20或切换为平台线程池(Executors.newFixedThreadPool(CONCURRENT_WORKERS))时无此问题。核心代码差异仅在于线程工厂:
- 平台线程池:
private static final ExecutorService taskExecutor = Executors.newFixedThreadPool(CONCURRENT_WORKERS);
- 虚拟线程池:
private static final ExecutorService taskExecutor = Executors.newFixedThreadPool(CONCURRENT_WORKERS, Thread.ofVirtual().factory());
线程池被包装为CompletionService,主线程批量提交任务后循环调用take()获取结果。
核心排查方向
结合httpclient-4.5.14的疑似关联点,重点围绕虚拟线程固定(Pinned Threads)和资源瓶颈展开:
1. 确认虚拟线程是否被固定
虚拟线程进入synchronized块/方法、调用本地方法时会被固定到平台线程,若大量虚拟线程被固定且占用所有可用平台线程,会导致任务调度停滞。
- 生成JSON格式线程dump(能完整展示虚拟线程状态):
查看每个虚拟线程的jcmd <进程PID> Thread.dump_to_file -format=json thread_dump.jsonstate和blockedOn字段,确认是否卡在httpclient的synchronized方法(如PoolingHttpClientConnectionManager的borrowConnection)。 - 强化固定线程追踪:
添加-Djdk.virtualThreadScheduler.maxPoolSize=20(增大平台线程池上限),同时保留-Djdk.tracePinnedThreads=full,运行后查看日志是否有固定线程的输出。
2. 排查httpclient连接池瓶颈
httpclient-4.5.x默认连接池配置为:全局最大连接数20,单路由最大连接数2。当50个虚拟线程同时发起请求时,会有大量线程阻塞等待连接,而这些阻塞的虚拟线程因处于synchronized块被固定到平台线程,最终耗尽平台线程资源,导致所有任务无法推进,主线程拿不到结果。
- 临时增大连接池配置验证:
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(100); // 全局最大连接数 connManager.setDefaultMaxPerRoute(50); // 单路由最大连接数 CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connManager).build();
3. 检查任务执行链路的阻塞点
- 确认
doTask方法除了http请求外,是否还有其他synchronized或阻塞逻辑(如数据库锁、文件IO),这些点也可能导致虚拟线程固定。 - 验证
storeData方法是否存在同步阻塞,若该方法持有锁且执行缓慢,会导致已完成任务的结果无法被及时处理,间接影响任务调度。
具体解决建议
1. 优化httpclient配置或版本
- 调整连接池参数:根据并发需求增大
maxTotal和defaultMaxPerRoute,减少线程等待连接的时间,降低虚拟线程被固定的概率。 - 升级到HttpClient 5.x:该版本用非阻塞锁(如
ReentrantLock)替代了部分synchronized代码,大幅减少虚拟线程固定的场景。
2. 调整虚拟线程调度器参数
通过JVM参数增大虚拟线程调度器的平台线程数,避免被固定的虚拟线程耗尽资源:
-Djdk.virtualThreadScheduler.parallelism=16 # 根据CPU核心数调整,如8核设为16 -Djdk.virtualThreadScheduler.maxPoolSize=30 # 平台线程池最大上限
3. 隔离阻塞性任务
将http请求这类会导致虚拟线程固定的阻塞逻辑,提交到单独的平台线程池执行,虚拟线程仅处理非阻塞业务:
// 单独创建平台线程池处理http请求 private static final ExecutorService httpExecutor = Executors.newFixedThreadPool(20); // doTask方法中调整为异步调用http请求 private Data doTask() throws Exception { CompletableFuture<HttpResponse> httpFuture = CompletableFuture.supplyAsync(() -> { // 执行http请求逻辑 return httpClient.execute(...); }, httpExecutor); HttpResponse response = httpFuture.join(); // 解析response并生成Data return parseData(response); }
4. 验证任务提交完整性
确认TASK_COUNT与实际提交的任务数一致,检查doTask是否会抛出未捕获异常(虽然CompletionService会接收异常的Future,但需确保所有任务都被成功提交到线程池)。
内容的提问来源于stack exchange,提问作者Abbadon

