使用JDK HttpClient发送请求时为何默认ForkJoin池总会启动?
在JDK17环境下使用内置HttpClient发送异步请求时,我指定了自定义线程池,但运行时总会额外启动两类线程:一是功能明确的独立SelectorManager线程,二是默认ForkJoin池线程。测试代码如下:
package test; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) throws IOException, InterruptedException, ExecutionException { var executor = Executors.newCachedThreadPool(); var client = HttpClient.newBuilder().executor(executor).build(); client.sendAsync( HttpRequest .newBuilder() .uri(URI.create("https://www.google.com")) .build(), HttpResponse.BodyHandlers.ofInputStream() ).get(); System.out.println("Done"); System.in.read(); } }
分析后推测,ForkJoin池的启动源于HttpClient内部的这段代码:
// makes sure that any dependent actions happen in the CF default // executor. This is only needed for sendAsync(...), when // exchangeExecutor is non-null. if (exchangeExecutor != null) { res = res.whenCompleteAsync((r, t) -> { /* do nothing */}, ASYNC_POOL); }
我猜想这段代码是为了将响应后续处理(可能包含阻塞操作)转移到其他线程池,避免阻塞HTTP数据处理线程,但有三个疑问需要解答:
疑问解答
为何不转移到指定的执行器而非始终使用ASYNC_POOL?
这是为了隔离HttpClient核心IO操作与用户自定义任务的线程环境。你指定的自定义执行器(exchangeExecutor)仅负责处理HTTP交换的核心流程:连接建立、请求发送、响应读取等IO密集型操作。如果将用户后续通过CompletableFuture链式调用添加的依赖操作(比如thenApply、thenAccept)放到这个自定义执行器中,一旦用户任务出现阻塞或线程耗尽,会直接影响HttpClient的核心IO性能,甚至引发死锁。JDK团队选择用全局默认的ForkJoin池(ASYNC_POOL)承载用户侧的后续操作,确保核心IO流程不受用户代码的干扰。对这段代码存在理由的猜想是否正确?
你的猜想方向正确,但更精准的解释是:这段代码的核心作用是重置CompletableFuture的默认执行器。当用自定义执行器完成HTTP交换后,返回的CompletableFuture会默认使用该自定义执行器来运行后续的依赖任务。通过whenCompleteAsync指定ASYNC_POOL,可以强制将后续链式调用的默认执行器恢复为全局ForkJoin池,保证sendAsync返回的Future符合CompletableFuture的通用行为(默认使用ForkJoin池),避免用户意外地将阻塞任务注入到HttpClient的IO线程池中。是否存在其他必须启动ForkJoin池的原因?
在你提供的测试场景中,ForkJoin池启动的唯一明确原因就是上述whenCompleteAsync的调用。HttpClient内部的其他辅助操作(如超时检测、连接池维护)是否会用到ForkJoin池取决于具体实现,但在这个极简测试用例里,没有其他触发ForkJoin池启动的因素。
内容的提问来源于stack exchange,提问作者adamw

