CompletableFuture的完成处理程序在哪个线程中执行?

我有一个关于CompletableFuture方法的问题:

public  CompletableFuture thenApply(Function fn) 

事情是JavaDoc说的就是这样:

返回一个新的CompletionStage,当该阶段正常完成时,将使用此阶段的结果作为所提供函数的参数执行。 有关特殊完成的规则​​,请参阅CompletionStage文档。

线程怎么样? 这将在哪个线程中执行? 如果未来由线程池完成怎么办?

CompletableFuture文档中指定的策略可以帮助您更好地理解:

  • 为非异步方法的依赖完成提供的动作可以由完成当前CompletableFuture线程执行, 或者由完成方法的任何其他调用者执行

  • 使用ForkJoinPool.commonPool()执行没有显式Executor参数的所有异步方法(除非它不支持至少两个并行级别,在这种情况下,创建一个新的Thread来运行每个任务 )。 为了简化监视,调试和跟踪,所有生成的异步任务都是标记接口CompletableFuture.AsynchronousCompletionTask实例。

更新 :我还建议@Mike阅读这个答案 ,作为对文档细节的进一步有趣分析。

正如@nullpointer指出的那样,文档会告诉您需要了解的内容。 但是,相关文本出乎意料地含糊不清,这里发布的一些评论(和答案)似乎依赖于文档不支持的假设。 因此,我认为将它拆开是值得的。 具体来说,我们应该非常仔细地阅读这一段:

为非异步方法的依赖完成提供的动作可以由完成当前CompletableFuture的线程执行,或者由完成方法的任何其他调用者执行。

听起来很简单,但细节很明显。 它似乎故意避免描述何时可以在完成线程上调用依赖完成,而不是像thenApply那样调用完成方法。 如上所述,上述段落实际上是要求我们用假设填补空白。 这是危险的,特别是当主题涉及并发和异步编程时,我们作为程序员开发的许多期望开始转向他们的头脑。 让我们仔细看一下文档没有说的内容。

该文档声明调用complete() 之前注册的依赖完成将在完成线程上运行。 此外,虽然它声明在调用thenApply类的完成方法时可能会调用依赖完成,但它并未声明将在注册它的线程上调用完成(请注意单词“any other”)。

对于使用CompletableFuture来安排和组合任务的任何人来说,这些都是潜在的重点。 考虑这一系列事件:

  1. 线程A通过f.thenApply(c1)注册从属完成。
  2. 一段时间后,线程B调用f.complete()
  3. 大约在同一时间,线程C通过f.thenApply(c2)注册另一个依赖完成。

从概念上讲, complete()做两件事:它发布未来的结果,然后尝试调用依赖的完成。 现在,如果线程C 结果值发布运行,但线程B绕过调用c1 之前会发生什么? 根据实现,线程C可能会看到f已完成,然后它可以调用c1 c2 。 或者,线程C可以调用c2同时让线程B调用c1 。 文档不排除这两种可能性。 考虑到这一点,以下是文档不支持的假设:

  1. 在调用f.complete()期间,将调用在完成之前f注册的依赖完成c ;
  2. 那个c将在f.complete()返回时运行完成;
  3. 将以任何特定顺序(例如,注册顺序)调用相关的完成;
  4. f完成之前注册的相关完成将 f完成之后注册的完成之前调用。

考虑另一个例子:

  1. 线程A调用f.complete() ;
  2. 一段时间后,线程B通过f.thenApply(c1)注册完成;
  3. 大约在同一时间,Thread C通过f.thenApply(c2)注册一个单独的完成。

如果已知f已经运行完成,则可能会假设在f.thenApply(c1)期间将调用f.thenApply(c1)并且在f.thenApply(c2)期间将调用f.thenApply(c2) 。 可以进一步假设c1将在f.thenApply(c1)返回时运行完成。 但是,文档支持这些假设。 调用thenApply的线程之一可能最终调用c1c2 ,而另一个线程不调用。

仔细分析JDK代码可以确定上述假设场景如何发挥作用。 但即使这样也存在风险,因为您最终可能依赖于(1)不可移植或(2)可能发生变化的实施细节。 你最好的选择是不要假设javadocs或原始JSR规范中没有说明的任何内容。

tldr:小心你的假设,当你写文档时,要尽可能清楚和慎重。 虽然简洁是一件很美妙的事情,但要注意填补空白的人类倾向。

来自Javadoc :

为非异步方法的依赖完成提供的动作可以由完成当前CompletableFuture的线程执行,或者由完成方法的任何其他调用者执行。

更具体地说:

  • fn将在调用complete()的任何线程的上下文中调用complete()

  • 如果complete()已经完成,那么调用thenApply()fn将在调用thenApply()的线程的上下文中运行。

说到线程,缺乏API文档。 理解线程和期货是如何工作需要一些推论。 从一个假设开始: CompletableFuture不会自己生成新线程。 工作将在现有线程下进行。

thenApply将在原始的CompletableFuture的线程中运行。 这可以是调用complete()的线程,也可以是调用thenApply()如果未来已经完成)。 如果你想控制线程 – 一个好主意,如果fn是一个慢速操作 – 那么你应该使用thenApplyAsync