CompletableFuture是如何提升Future性能的

6,671 阅读3分钟

Future优点

Future优点在于我们可以异步地进行一些非常密集的计算,而不会阻塞当前的线程,这样,我们在此期间就可以做一些其他的工作。

但是,当获取结果的时候,future想要获取结果的时候,会在主线程中阻塞住。同时,考虑下多个 Future的场景。如果我们有多了 Future,而且这些 Future之间产生关系。

  1. 场景1:第一个 Future 的返回值是第二个 Future 的输入

  2. 场景2:创建三个 Future,f1需要20s,f2需要5s,f3需要10s,然后我们将他们list.add(f1);list.add(f2);list.add(f3),再依次fx.get(),你会发现,即使f2先执行完,也要等f1执行完,f2.get才能返回。

下面罗列了 Future 几个缺点:

Future缺点

  1. Future.get方法虽然可以设置超时时间,但是在超时时间到来前无法手动结束或完成

  2. Future provides a get() method which blocks until the result is available. further action can not be performed on a Future’s result without blocking the primary application thread Future.get()方法是阻塞的,直到有返回值返回。也就是说在不阻塞主应用程序线程的情况下,无法对 Future 的结果执行进一步的操作

  3. Asynchronous workflows can not be created by chaining multiple Futures together. 多个 Future 不能链(chain)在一起来创建异步工作流

  4. Futures which are running in parallel, can not be combined together. 并行运行的 Future 不能合并在一起

  5. Future API does not have any exception handling construct. Future API 没有异常处理逻辑

举例说明

实例1:stringFuture.get()无法手动停止或完成

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> stringFuture = executor.submit(() -> neverEndingComputation());
System.out.println("The result is: " + stringFuture.get());

如上代码,stringFuture.get()永远不会有返回值。

实例2: 多个 Future 之间存在依赖关系时

第一个 Future 的返回值是第二个 Future 的输入,代码如下:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> firstApiCallResult = executor.submit(
    () -> firstApiCall(someValue)
);
   
String firstResult = firstApiCallResult.get(); // 主线程阻塞

Future<String> secondApiCallResult = executor.submit(
    () -> secondApiCall(firstResult)
);

如上代码,可以看到,第二个 Future 需要等待第一个 Future的返回值,而且第一个 Future 的返回值是在主线程中阻塞获取的。

20230319150809

CompletableFuture如何解决Future缺点的

实例1的答案

针对实例1的问题,CompletableFuture 如何解决的呢。CompletableFuture 有个 complete(String)方法,他可以手动结束执行中的任务。回顾下实例1的代码及 CompletableFuture 的代码

  • 实例1
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> stringFuture = executor.submit(() -> neverEndingComputation());
System.out.println("The result is: " + stringFuture.get());
  • 解决实例1问题的代码
CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> neverEndingComputation());
stringCompletableFuture.complete("Completed");
System.out.println("Is the stringCompletableFuture done ? " + stringCompletableFuture.isDone());
  • result: Is the stringCompletableFuture done ? true

查看下 CompletableFuture.complete(arg)的源码注释就明白了

实例2的答案

实例2的问题是两个有关联的 Future如何能真正做到异步呢。CompletableFuture的链式(chain)方法就是答案。

  • 实例2
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> firstApiCallResult = executor.submit(
    () -> firstApiCall(someValue)
);
   
String firstResult = firstApiCallResult.get(); // 主线程阻塞

Future<String> secondApiCallResult = executor.submit(
    () -> secondApiCall(firstResult)
);
  • 解决实例2问题的代码
var finalResult = CompletableFuture.supplyAsync(
     () -> firstApiCall(someValue)
).thenApply(firstApiResult -> secondApiCall(firstApiResult));

可以看到,使用CompletableFuture链式(chain)方法期间,没有和主线程有任何交互。更进一步,你可以在每个链式(chain)方法中打印下线程名,你会发现都不是主线程名。也就是说,CompletableFuture链式(chain)方法完全做到了全程无阻塞。

可以看到,CompletableFuture 与 Java Streams 非常相似。

it`s time to summary

CompletableFuture 解决了 Future 在多个 Future 有关联的场景下的不足。同时,CompletableFuture也可以主动/手动去结束/完成异步任务。而且,CompletableFuture提供了非常丰富的方法。

参考:java-completablefuture