ExecutorService vs CompletableFuture

Posted

技术标签:

【中文标题】ExecutorService vs CompletableFuture【英文标题】: 【发布时间】:2019-02-17 14:01:55 【问题描述】:

我一直在尝试实现一个异步过程,其中父方法调用一个子方法,该子方法又会调用三个不同的方法。 我希望所有这些过程都异步完成,即在子方法中的这三个调用并行进行之后,控件应该返回父方法并继续执行其余部分。

我有这段代码,经过测试可以正常工作。

public ReturnSomething parent()
    child();
    ...//rest to UI


private void child()
  ExecutorService executorService = Executors.newFixedThreadPool(3);

  Runnable service1 = () -> 
     MyFileService.service1();
  ;

  Runnable service2 = () -> 
      MyFileService.service2();
  ;

  Runnable service3 = () -> 
      MyFileService.service3();
  ;

  executorService.submit(service1);
  executorService.submit(service2);
  executorService.submit(service3);

现在,我的领导要求我改用这个。

public ReturnSomething parent()
    child();
    ...//rest to UI


private void child()
    CompletableFuture.supplyAsync(() ->  MyFileService.service1();
    CompletableFuture.supplyAsync(() ->  MyFileService.service2();
    CompletableFuture.supplyAsync(() ->  MyFileService.service3();

我知道 CompletableFuture 是 Java 8 的新功能,但是第二个代码比第一个代码更好吗? 因为对于 ExecutorService,我没有调用“get()”方法,所以我不会等待 aysnc 响应。那么,有人能解释一下有什么区别吗?

【问题讨论】:

【参考方案1】:

如果你正在使用 executorservice,不要忘记在 executor 上调用 shutdown()。您也可以使用 runAsync() 而不是 supplyAsync()。

【讨论】:

【参考方案2】:

从功能上讲,这两种方法大致相同:

您提交任务以供执行; 您无需等待结果。

然而,技术上存在一些细微的差别:

在第二种方法中,您没有指定执行者,因此它将使用常见的ForkJoinPool。如果您不想这样做,则必须将执行程序作为 supplyAsync() 的第二个参数传递; CompletableFuture API 允许使用thenApply()thenCompose() 等轻松链接更多调用。因此它比ExecutorService.submit() 返回的简单Future 更灵活; 使用CompletableFuture 允许使用return CompletableFuture.allOf(the previously created futures) 从您的child() 方法轻松返回未来。

关于可读性,这是一个偏好问题,但如果您想要等效的代码,则CompletableFuture 方法在您对其进行类似格式化后可能会被认为可读性较差。比较:

executorService.submit(MyFileService::service1);
executorService.submit(MyFileService::service2);
executorService.submit(MyFileService::service3);

CompletableFuture.supplyAsync(MyFileService::service1, executorService);
CompletableFuture.supplyAsync(MyFileService::service2, executorService);
CompletableFuture.supplyAsync(MyFileService::service3, executorService);

【讨论】:

有区别。 CompletableFuture 在可能导致意外行为的背后使用 ForkJoinPool#commonPool。请在此处阅读更多信息dzone.com/articles/be-aware-of-forkjoinpoolcommonpool @walv 这已经在答案中提到了 是的,上面写着so it will use the common ForkJoinPool,但它没有解释在后台使用 ForkJoinPool 可能会导致意外行为,这就是为什么我在链接中添加我的评论以便人们阅读的原因。请把我的评论作为对你答案的补充。【参考方案3】:

首先,后者提高了可读性。其次,我不确定您是否急于(以通用方式解释您的问题)每次 parent() 调用 child() 时都创建了一个新的 ExecutorService 实例。

否则,CompletableFuture.supplyAsync 也会返回一种非常方便的方式来获取对公共共享池的引用,这将使您的生活更加轻松,除非您想对池中的线程应该为您的请求提供服务的方式进行特定的自定义。

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html#commonPool--

【讨论】:

可读性的提高仅仅是由于 OP 格式化他的代码并在 CompletableFuture 示例中内联 lambda 的方式。它并非来自 API——事实上恰恰相反。【参考方案4】:

在这两种情况下,您都无需等待结果。

第二种方法的优点是样板文件更少。这就是 runAsync()supplyAsync() 的优势所在。

但是如果你实际上没有返回任何值,你应该使用runAsync()

第二种方法还提供了使用CompletableFuture.allOf() 等待所有期货的能力。在第一个场景中也不存在。

【讨论】:

它的样板更少,因为它使用常见的ForkJoinPool。如果您想使用自己的执行程序,它会变得更加样板。关于等待所有期货的能力,也可以使用第一种方法来做到这一点:只需在每个期货上调用get()allOf() 在您想要链接更多调用或返回它时更有用。

以上是关于ExecutorService vs CompletableFuture的主要内容,如果未能解决你的问题,请参考以下文章

Java线程之CompletionService

c_cpp 最小窗口子串。给定一个字符串S和一个字符串T,找到S中的最小窗口,它将包含在Comple中的T中的所有字符

ExecutorService线程池讲解

在 ExecutorService 的提交和 ExecutorService 的执行之间进行选择

java并发中ExecutorService的使用

Java 中这段代码中的 ExecutorService.submit 和 ExecutorService.execute 有啥区别?