Java 11 HTTP客户端异步执行

23
我正在尝试使用JDK 11的新HTTP客户端API,特别是它异步执行请求的方式。但有一些我不确定是否理解的事情(一种实现方面)。在文档中,它说:

返回的CompletableFuture实例的异步任务和依赖操作在客户端提供的Executor线程上执行,如果可行。

据我理解,这意味着如果我在创建HttpClient对象时设置了自定义执行程序,则会在该执行程序提供的线程上执行CompletableFuture实例的异步任务和依赖操作:

ExecutorService executor = Executors.newFixedThreadPool(3);

HttpClient httpClient = HttpClient.newBuilder()
                      .executor(executor)  // custom executor
                      .build();

那么,如果我异步地发送一个请求,并在返回的CompletableFuture上添加依赖动作,则这些依赖动作应该在指定的执行器上执行。

httpClient.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
      System.out.println("Thread is: " + Thread.currentThread().getName());
      // do something when the response is received
});

然而,在上述的依赖操作(即thenAccept中的consumer)中,我看到执行它的线程来自于公共池而不是自定义执行器,因为它打印了Thread is: ForkJoinPool.commonPool-worker-5
这是实现中的错误吗?还是我漏掉了什么?我注意到它说“实例在客户端提供的Executor上执行,如果可行的话”,那么这是否是一个没有应用这一点的情况?
请注意,我还尝试了thenAcceptAsync,结果相同。

抱歉,如果这很愚蠢,请帮我理解一下,你是如何解释“它来自公共池而不是自定义执行器,因为它打印线程是:ForkJoinPool.commonPool-worker-5”?...我还尝试在“thenAccept”消费者中使用System.out.println(httpClient.executor().get().equals(executor));,它打印出“true”。 - Naman
2
@nullpointer 我猜他在 thenAcceptConsumer 中打印了 Thread.currentThread().getName(),而线程名称表明该 Thread 来自公共的 ForkJoinPool 而不是自定义的 Executor。换句话说,OP 并没有说 HttpClientExecutor 已经 _改变_,而是想知道为什么依赖的 CompletableFuture 阶段会使用不同的线程池来执行。 - Slaw
1
@nullpointer 正如Slaw所说的那样。我也知道该线程是来自公共池,因为我可以为自定义执行程序创建的线程提供特殊名称以清晰地标识它们。至于 httpClient.executor(),此方法只返回我在创建时指定的执行程序,而这不是 thenAccept 使用的执行程序。 - M A
@Slaw @manouti 谢谢。我明白你们两个指向的方向了,确实尝试为执行器提供自定义命名线程,并且发现它在 thenAccept 中没有被使用。我会进一步寻找关于“在实践中”的细节以及错误数据库的信息。 - Naman
1
事实证明,在此API的进展过程中,文档已经更新,因此它描述了这种行为。更近期的文档链接是https://download.java.net/java/early_access/jdk11/docs/api/java.net.http/java/net/http/package-summary.html - M A
2个回答

15
我刚刚找到了一份更新的文档(我最初提供的那个似乎已经过时了),其中解释了这种实现行为:

通常,异步任务在调用操作的线程中执行,例如发送HTTP请求,或由客户端的执行器提供的线程执行。 依赖任务,即由返回的CompletionStages或CompletableFutures触发的任务,如果没有明确指定执行器,则在与CompletableFuture相同的默认执行器中执行,或者如果操作在注册依赖任务之前完成,则在调用线程中执行。

CompletableFuture的默认执行器是公共池。
我还找到了引入此行为的bug ID,其中API开发人员充分解释了它。
2) 依赖任务在公共池中运行 默认情况下,依赖任务的执行已更新为在与CompletableFuture的defaultExecutor相同的执行器中运行。这对于已经使用CF的开发人员更加熟悉,并减少了HTTP客户端被耗尽线程来执行其任务的可能性。这只是默认行为,如果需要,HTTP客户端和CompletableFuture都允许更细粒度的控制。

“这只是默认行为,如果需要的话,HTTP客户端和CompletableFuture都允许更细粒度的控制。对于CompletableFuture,我认为他指的是使用*Async(…, Executor)方法变体。但是对于HTTP客户端,我不知道它如何帮助控制此行为。我觉得在任何地方都使用异步方法变体并不是很方便。” - Didier L
当我读到这一点时,我也是这样想的;只有构建器允许在HTTP客户端中传递执行器,这甚至不会影响CF中的依赖任务。此外,即使最近的文档似乎也不一致,因为它仍然说“设置用于异步和依赖任务的执行器”。也许他们忘记更新那部分了。 - M A
1
我赞同@manouti的答案,它非常准确。同时,以下错误已经被记录下来以修复有关依赖任务在执行器中运行的不当规范,https://bugs.openjdk.java.net/browse/JDK-8209943 - chegar999
@chegar999 很酷!感谢您努力推出这个API!顺便说一下,我在这篇文章[https://mahmoudanouti.wordpress.com/2018/08/20/new-java-http-client/]中总结了一些使用API的示例,在此期间我遇到了这种行为。 - M A

11

简化版本: 我认为你已经确定了一个实现细节,"在可行的情况下"的意思是不能保证会使用提供的executor

详细说明:

我从这里下载了JDK 11源代码。(写作时为jdk11-f729ca27cf9a)。

src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java中有以下类:

/**
 * A DelegatingExecutor is an executor that delegates tasks to
 * a wrapped executor when it detects that the current thread
 * is the SelectorManager thread. If the current thread is not
 * the selector manager thread the given task is executed inline.
 */
final static class DelegatingExecutor implements Executor {

如果 isInSelectorThread 为真,则此类使用 executor ,否则任务将在内联执行。这可以简化为:

boolean isSelectorThread() {
    return Thread.currentThread() == selmgr;
}

其中 selmgr 是一个 SelectorManager编辑:该类也包含在HttpClientImpl.java中:

// Main loop for this client's selector
private final static class SelectorManager extends Thread {

总之,我猜测“在实践中”意味着它取决于实现,并且不能保证提供的executor会被使用。

注意:这与默认的执行程序不同,在那种情况下,构建器不提供executor。换句话说,如果构建器提供了executor,则会进行SelectorManager的身份验证。


1
感谢您的回答。这似乎确实是一个实现细节。然而,关于您最后的注释,当我在客户端设置中不指定执行器时,依赖操作仍然使用公共池,而不是默认的线程缓存执行器。 - M A

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接