使用Project Loom时ExecutionCompletionService挂起问题排查求助
尝试使用Project Loom实现REST服务器的并行调用,需处理约50000次API请求,按500条为一批进行处理。如下代码片段中,使用虚拟线程时ExecutorService会出现挂起:通过take()方法获取约490+个响应后就停滞,但使用传统系统线程无此问题,且批次规模较小时也能正常运行。
public List<ApiResponseWrapper> sendRequestToApi(List<ApiRequest> apiRequests) throws ExecutionException, InterruptedException { ThreadFactory factory = Thread.ofVirtual().factory(); List<ApiResponseWrapper> apiResponseWrappers = new ArrayList<>(); final ExecutorService pool = Executors.newFixedThreadPool(7, factory); final CompletionService<ApiEntityResponseWrapper> service = new ExecutorCompletionService<>(pool); List<RestTemplateClient> webClientCallables = apiRequests.stream().map(ApiRequest -> new RestTemplateClient(ApiRequest,restTemplate)).toList(); webClientCallables.forEach(service::submit); pool.shutdown(); int i = 0; try{ for(RestTemplateClient webClient : webClientCallables){ ApiResponseWrapper apiEntityResponseWrapper = service.take().get(); apiResponseWrappers.add(apiEntityResponseWrapper); log.info("The size in webclient callables are reduced by {}", webClientCallables.size()-i); i++; } } catch (InterruptedException | ExecutionException | TibcoClientException ee){ log.error("An exception occurred while processing requests towards Tibco ", ee.getCause()); throw ee; } return apiResponseWrappers; }
你的代码出现挂起的核心原因集中在虚拟线程特性与RestTemplate底层资源的交互冲突,具体拆解如下:
1. 底层连接池资源耗尽导致任务阻塞
RestTemplate默认依赖HttpURLConnection,其内置连接池对单目标主机的并发连接数有默认限制(通常为5-20)。你用FixedThreadPool(7)创建虚拟线程池,虽然线程数固定为7,但虚拟线程的调度机制会在IO阻塞时让载体线程切换处理其他任务,看似能并行推进更多请求,实则底层连接池的并发上限并未改变。当处理500个请求时,大量任务会排队等待空闲连接,部分任务因连接池耗尽陷入无限阻塞,导致CompletionService.take()一直等待未完成的任务。
而系统线程池场景下,7个系统线程同时发起请求,连接池默认配置刚好能支撑,任务完成后会及时释放连接,因此不会挂起;小批次请求未触及连接池上限,自然也能正常运行。
2. 固定虚拟线程池的不合理性
虚拟线程的设计初衷是为每个阻塞IO任务创建独立虚拟线程,利用其轻量特性承载高并发。你用FixedThreadPool(7)限制虚拟线程数量,完全浪费了虚拟线程的优势,反而让任务排队逻辑和系统线程池无异。且虚拟线程的调度机制在固定池模式下,可能出现任务调度优先级异常,导致部分任务无法被及时唤醒处理。
3. 潜在的ThreadLocal状态冲突
如果RestTemplate或其底层客户端依赖ThreadLocal存储连接状态,虚拟线程复用载体线程时,可能出现ThreadLocal状态泄露或冲突。比如某个连接被错误绑定到多个虚拟线程,导致连接无法正常释放,最终耗尽连接池资源。
- 替换ExecutorService类型:使用
Executors.newVirtualThreadPerTaskExecutor()替代固定大小的虚拟线程池,让每个请求对应一个虚拟线程,充分发挥虚拟线程的并发优势。 - 自定义连接池配置:切换到Apache HttpClient或OkHttp作为RestTemplate的底层客户端,显式配置足够的最大连接数、连接超时和读取超时,避免因连接耗尽导致任务阻塞。
- 添加全局异常捕获:在
RestTemplateClient的call()方法中添加全局异常捕获,确保所有异常都被封装到Future中,避免任务因未捕获异常陷入未知状态。 - 调整批次处理逻辑:可适当降低单批次请求量,或配合连接池的动态扩容策略,平衡并发量与资源占用。
内容的提问来源于stack exchange,提问作者Hari R

