HttpClient Executors工作原理 - Cay Horstmann

19-09-02 banq
                   

 Java 11添加了HttpClient,为我们提供了一种更好的HTTP请求发送方式。它支持异步和同步模式。支持HTTP2开箱即用。有点时髦,Cay Horstmann教授探讨了如何在表面下的工作原理。

JCrete 2019年,Heinz Kabutz主持了一个演讲,展示了为HttpClient该类配置线程池的谜团。设置新的执行程序没有达到预期的效果。事实证明,实施已经发生了变化(也许并没有变得更好),而且文档也是滞后的。如果您计划HttpClient异步使用,您真的要注意这一点。

HttpClient Executors

HttpClient是Java 9中的孵化器功能,最终成为Java 11中Java API的一部分。它提供了比经典HttpURLConnection类更令人愉快的API ,具有良好的异步接口,并且可以使用HTTP / 2。本文讨论异步接口。

假设你想要阅读一个网页,然后一旦它访问到就处理它。首先做一个HttpClient对象:

HttpClient client = HttpClient.newBuilder()
  // Redirect except https to http
  .followRedirects(HttpClient.Redirect.NORMAL) 
  .build();

然后执行一个请求:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("http://horstmann.com"))
  .GET()
  .build();
  

现在获取响应并处理它,添加sendAsync方法返回的未来可完成(completable future)的结果:

client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
  .thenAccept(response -> ...);

sendAsync方法使用非阻塞I / O来获取数据。当数据可用时,数据将被传递给回调函数立即进行处理。HttpClient使用标准的 CompletableFuture接口。传递给thenAccept的回调函数在数据准备好时调用。

在哪个线程?当然不是在调用的线程中 client.sendAsync。因为这个线程已经开始做其他事情。

HttpClient.Builder类里有一个方法 executor:

ExecutorService executor1 = Executors.newCachedThreadPool();
HttpClient client = HttpClient.newBuilder()
  .executor(executor1)
  .followRedirects(HttpClient.Redirect.NORMAL)
  .build();
  

根据JDK 11文档,这“将executor 执行器设置为用于异步和依赖任务”。

Heinz的图像采集者之谜

在JCrete 2019年,Heinz Kabutz演示了一个程序,它抓住当天的Dilbert漫画,转到表格的URL https://dilbert.com/strip/2019-08-21,找到里面的图像URL,然后加载图像。

但它没有用。在我的Linux笔记本电脑上,该程序刚挂起,而在Heinz的Mac上,它在尝试获取10,000张图像时因内存不足而崩溃。

Executor

Heinz并不是第一个注意到设置执行程序不能按预期工作的人 - 看到这个StackOverflow查询。这是自JDK 11以来的行为变化。现在,“依赖”任务不是 由提供的执行程序执行,而是由公共fork-join池执行。但是,文档尚未更新以跟踪更改,这是另一个错误

让我们从变更通知中挑选重点的陈述:

  • “对于已经使用CF的开发人员来说,这更为熟悉”。不要赌它。通过在公共池上运行阻塞任务来使公共池饿死是一个常见的错误。
  • “降低了HTTP客户端缺乏执行其任务的线程的可能性”。HTTP客户端需要线程来管理选择器及其响应,当然这些线程永远不会被饿死。假设相同的执行程序可能适用于HTTP客户端的内部工作和处理其结果的任务(如此错误报告中所述),这是愚蠢的。人们希望这不是设计的最初意图,除了“异步和依赖任务”的措辞暗示它可能已经存在。
  • “这只是默认行为,如果需要,HTTP Client和CompletableFuture都允许更细粒度的控制。” 实际上,如您所见,该executor方法为内部工作设置执行程序。您可以通过指定执行程序来控制相关任务:

    return client.sendAsync(request, responseBodyHandler)
      .thenApplyAsync(HttpResponse::body, executor2);
    

以下是要点:

  • 除非您知道公共fork-join池是该任务的正确执行程序,否则就为sendAsync分配一个executor 执行器 。
  • 不要调用HttpClient builder上的executor执行器,除非你知道你的executor是更好的(大概是在研究之后和理解 源代码 中的HttpClient实现)。

HttpClient实现使用缓存线程池cached thread pool 来完成其任务。在Linux上,当获取10,000个图像时,HttpClient executor 永远不会有超过几百个并发任务,在Mac上,虚拟机在创建超过2,000个线程后内存不足,当提供固定的线程池 fixed thread pool时,程序挂在Mac上,就像在Linux上一样。

当进行 completable futures链式调用时,确保处理异常,尤其是在您需要管理计数器或资源时。

点击标题见原文详细分析