一、CompletableFuture
1. Future的局限性
从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:
- 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;
- 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
- 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
- 没有异常处理:Future接口中没有关于异常处理的方法;
2. CompletableFuture与Future的不同
简单地说,CompletableFuture是Future接口的扩展和增强。CompletableFuture完整地继承了Future接口,并在此基础上进行了丰富地扩展,完美地弥补了Future上述的种种问题。更为重要的是,CompletableFuture实现了对任务的编排能力。借助这项能力,我们可以轻松地组织不同任务的运行顺序、规则以及方式。从某种程度上说,这项能力是它的核心能力。而在以往,虽然通过CountDownLatch等工具类也可以实现任务的编排,但需要复杂的逻辑处理,不仅耗费精力且难以维护。
二、CompletableFuture的核心设计
总体而言,CompletableFuture实现了Future和CompletionStage两个接口,并且只有少量的属性。
Future接口仅提供了get()和isDone这样的简单方法,仅凭Future无法为CompletableFuture提供丰富的能力。那么,CompletableFuture又是如何扩展自己的能力的呢?这就不得不说CompletionStage接口了,它是CompletableFuture核心,也是我们要关注的重点。
顾名思义,根据CompletionStage名字中的“Stage”,你可以把它理解为任务编排中的步骤。所谓步骤,即任务编排的基本单元,它可以是一次纯粹的计算或者是一个特定的动作。在一次编排中,会包含多个步骤,这些步骤之间会存在依赖、链式和组合等不同的关系,也存在并行和串行的关系。这种关系,类似于Pipeline或者流式计算。
CompletableFuture<String> base = new CompletableFuture<>(); CompletableFuture<String> future = base.thenApply(s -> s + " 2").thenApply(s ->s + " 3"); base.complete("1"); log.info(future.get());
CompletableFuture提供了多达50多个方法,全部理解比较困难。但在理解时仍有规律可循,我们可以通过分类的方式简化对方法的理解。
根据类型,这些方法可以总结为以下四类,其他大部分方法都是基于这四种类型的变种:
关于方法的变种
上述各种类型的方法一般都有三个变种方法:同步、异步和指定线程池。比如, thenApply()的三个变种方法如下所示:
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn,Executor executor)
下面这幅类图,展示了CompletableFuture和Future、CompletionStage以及Completion之间的关系。
当然,由于方法众多,这幅图中并没有全部呈现,而是仅选取了部分重要的方法。
三、CompletableFuture的核心用法
前面已经说过,CompletableFuture的核心方法总共分为四类,而这四类方法又分为两种模式:同步和异步。
- 同步:使用当前线程运行任务;
- 异步:使用CompletableFuture线程池其他线程运行任务,异步方法的名字中带有Async.
1. runAsync(不接收参数、不返回参数)
runAsync()是CompletableFuture最常用的方法之一,它可以接收一个待运行的任务并返回一个CompletableFuture;
当我们想异步运行某个任务时,以往需要手动实现Thread或者借助Executor实现。而通过runAsync()就简单多了。比如,我们可以直接传入Runnable类型的任务:
CompletableFuture.runAsync(new Runnable() { @Override public void run() { note("妲己进入草丛蹲点...等待小鲁班出现"); } });
2. supply与supplyAsync(不接收参数,返回结果)
对于supply()这个方法,它会返回一个结果,并且这个结果可以被后续的任务所使用。
举个例子,在下面的示例代码中,我们通过supplyAsync()返回了结果,而这个结果在后续的
thenApply()被使用。
// 创建nameFuture,返回姓名 CompletableFuture <String> nameFuture = CompletableFuture.supplyAsync(() -> { return "妲己"; }); // 使用thenApply()接收nameFuture的结果,并执行回调动作 CompletableFuture <String> sayLoveFuture = nameFuture.thenApply(name -> { return "鲁班," + name; }); //阻塞获得结果 System.out.println(sayLoveFuture.get()); // 鲁班,妲己
3. thenApply与thenApplyAsync(接收结果返回结果,链式调用)
thenApply()用于接收supply()的执行结果,并执行特定的代码逻辑,最后返回CompletableFuture结
果。
// 创建nameFuture,返回姓名 CompletableFuture <String> nameFuture = CompletableFuture.supplyAsync(() -> { return "妲己"; }); // 使用thenApply()接收nameFuture的结果,并执行回调动作 CompletableFuture <String> sayLoveFuture = nameFuture.thenApply(name -> { return "爱你," + name; }); public <U> CompletableFuture <U> thenApplyAsync( Function <? super T, ? extends U> fn) { return uniApplyStage(null, fn); });
4. thenAccept与thenAcceptAsync(接收结果,但不返回结果,链式调用)
thenAccept()只接收数据,但不会返回结果,它的返回类型是Void.
CompletableFuture<Void> sayLoveFuture = nameFuture.thenAccept(name -> { System.out.println("爱你," + name); }); public CompletableFuture < Void > thenAccept(Consumer < ? super T > action) { return uniAcceptStage(null, action); }
5. thenRun(不接收结果、不返回结果)
thenRun()就比较简单了,不接收任务的结果,只运行特定的任务,并且也不返回结果。
public CompletableFuture < Void > thenRun(Runnable action) { return uniRunStage(null, action); }
所以,如果你在回调中不想返回任何的结果,只运行特定的逻辑,那么你可以考虑使用thenAccept和thenRun. 一般来说,这两个方法会在调用链的最后面使用。
6. thenCombine
以上几种方法都是一元依赖关系,不能解决两元依赖关系
举个例子,当我们计算某个英雄(比如妲己)的胜率时,我们需要获取她参与的总场次(rounds),以及她获胜的场次(winRounds),然后再通过winRounds / rounds来计算。对于这个计算,我们可以这么做:
CompletableFuture < Integer > roundsFuture = CompletableFuture.supplyAsync(() -> 500); CompletableFuture < Integer > winRoundsFuture = CompletableFuture.supplyAsync(() -> 365); CompletableFuture < Object > winRateFuture = roundsFuture .thenCombine(winRoundsFuture, (rounds, winRounds) -> { if (rounds == 0) { return 0.0; } DecimalFormat df = new DecimalFormat("0.00"); return df.format((float) winRounds / rounds); }); System.out.println(winRateFuture.get());
thenCombine()将另外两个任务的结果同时作为参数,参与到自己的计算逻辑中。在另外两个参数未就绪时,它将会处于等待状态。
7. allOf与anyOf
当我们需要对多个Future的运行进行组织时,就可以考虑使用它们:
- allOf():给定一组任务,等待所有任务执行结束;
- anyOf():给定一组任务,等待其中任一任务执行结束。
allOf()与anyOf()的方法签名如下:
static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
需要注意的是,anyOf()将返回完任务的执行结果,但是allOf()不会返回任何结果,它的返回值是Void.allOf()与anyOf()的示例代码如下所示。我们创建了roundsFuture、winRoundsFuture、addFuture,并通过sleep模拟它们的执行时间。在执行时,winRoundsFuture将会先返回结果,所以当我们调用CompletableFuture.anyOf时也会发现输出的是365.
CompletableFuture < Integer > roundsFuture = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(200); return 500; } catch (InterruptedException e) { return null; } }); CompletableFuture < Integer > winRoundsFuture =CompletableFuture.supplyAsync(() -> { try { Thread.sleep(100); return 365; } catch (InterruptedException e) { return null; } }); CompletableFuture < Integer > addFuture = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(900); return 900; } catch (InterruptedException e) { return null; } }); CompletableFuture < Object > completedFuture = CompletableFuture.anyOf(winRoundsFuture, roundsFuture); System.out.println(completedFuture.get()); // 返回365
在CompletableFuture之前,如果要实现所有任务结束后执行特定的动作,我们可能会使用
CountDownLatch等工具类。现在,我们也可以考虑使用CompletableFuture.allOf.
四、CompletableFuture中的异常处理
对于任何框架来说,对异常的处理都是必不可少的,CompletableFuture当然也不会例外。前面,我们已经了解了CompletableFuture的核心方法。现在,我们再来看如何处理计算过程中的异常。
考虑下面的情况,当rounds=0时,将抛出运行时异常。此时,我们应该如何处理?
CompletableFuture < Void > completedFutures = CompletableFuture.allOf(winRoundsFuture, roundsFuture); CompletableFuture < ? extends Serializable > winRateFuture = roundsFuture.thenCombine(winRoundsFuture, (rounds, winRounds) -> { if (rounds == 0) { throw new RuntimeException("总场次错误"); } DecimalFormat df = new DecimalFormat("0.00"); return df.format((float) winRounds / rounds); }); System.out.println(winRateFuture.get());
在CompletableFuture链式调用中,如果某个任务发生了异常,那么后续的任务将都不会再执行。对于异常,我们有两种处理方式:exceptionally()和handle().
1. 使用exceptionally()回调处理异常
在链式调用的尾部使用exceptionally(),捕获异常并返回错误情况下的默认值。需要注意的是,
exceptionally()仅在发生异常时才会调用。
CompletableFuture < ? extends Serializable > winRateFuture = roundsFuture.thenCombine(winRoundsFuture, (rounds, winRounds) -> { if (rounds == 0) { throw new RuntimeException("总场次错误"); } DecimalFormat df = new DecimalFormat("0.00"); return df.format((float) winRounds / rounds); }).exceptionally(ex -> { System.out.println("出错:" + ex.getMessage()); return ""; }); System.out.println(winRateFuture.get());
2. 使用handle()处理异常
除了exceptionally(),CompletableFuture也提供了handle()来处理异常。不过,与exceptionally()不同的是,当我们在调用链中使用了handle(),那么无论是否发生异常,都会调用它。所以,在handle()方法的内部,我们需要通过 if (ex != null) 来判断是否发生了异常。
CompletableFuture < ? extends Serializable > winRateFuture = roundsFuture.thenCombine(winRoundsFuture, (rounds, winRounds) -> { if (rounds == 0) { throw new RuntimeException("总场次错误"); } DecimalFormat df = new DecimalFormat("0.00"); return df.format((float) winRounds / rounds); }).handle((res, ex) -> { if (ex != null) { System.out.println("出错:" + ex.getMessage()); return ""; } return res; }); System.out.println(winRateFuture.get());
当然,如果我们允许某个任务发生异常而不中断整个调用链路,那么可以在其内部通过try-catch消化掉。
五 CompletableFuture的工作流
CompletableFuture初始化时可以处于completed和incompleted两种状态,先看两个最简单的例子。
初始化就completed
// base直接初始化成一个已完成的CompletableFuture,完成值是"completed" CompletableFuture<String> base = CompletableFuture.completedFuture("completed"); log.info(base.get());
输出:
[INFO ] [2019-07-15 10:05:13] [main] completed
这里base对象是一个已完成的CompletableFuture,所以get()直接返回了"completed"。当然如果初始化时用了未完成的CompletableFuture,那么get()方法是会阻塞等待它完成。
初始化后主动complete
我们也可以在之后的代码中,或是其他线程中将它“完成”:
// 这是一个未完成的CompletableFuture CompletableFuture<String> base = new CompletableFuture<>(); log.info("start another thread to complete it"); new Thread(() -> { log.info("will complete in 1 sec"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } base.complete("completed"); }).start(); log.info(base.get());
输出:
[INFO ] [2019-07-15 14:32:26] [main] start another thread to complete it [INFO ] [2019-07-15 14:32:26] [Thread-0] will complete in 1 sec [INFO ] [2019-07-15 14:32:27] [main] completed
这个例子中主线程在调用get()方法时阻塞,Thread-0线程在sleep 1秒后调用complete()方法将base完成,主线程get()返回得到完成值completed。
异常complete
CompletableFuture<String> base = new CompletableFuture<>(); base.completeExceptionally(new RuntimeException(("runtime error"))); log.info(base.get());
输出:
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: runtime error at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357) at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895) at com.aliyun.completable.Main.main(Main.java:33) Caused by: java.lang.RuntimeException: runtime error at com.aliyun.completable.Main.main(Main.java:32)
在complete时发生异常,在base调用get()方法时抛出ExecutionException。
小结
我们可以得出最基本的一个流程,CompletableFuture是靠complete作为一个初始力来驱动的,虽然这不是它的全部,但至少得complete它才会去继续执行后面依赖它的一系列处理。
六、任务的依赖关系(thenApply)
先看如下代码:
CompletableFuture<String> base = new CompletableFuture<>(); CompletableFuture<String> future = base.thenApply(s -> s + " 2").thenApply(s ->s + " 3"); base.complete("1"); log.info(future.get());
输出:
[INFO ] [2019-07-15 15:15:44] [main] 1 2 3
可能大家也觉得这个输出是完全可预期的,那么再看看下面输出是什么:
CompletableFuture<String> base = new CompletableFuture<>(); CompletableFuture<String> future = base.thenApply(s -> s + " 2").thenApply(s ->s + " 3");
future.complete("1"); log.info(future.get()); 输出:11:12:38 [INFO ] [main] l.n.CompleteTest1 - 1
// base.complete("1"); // log.info(base.get()); 输出:11:13:18 [INFO ] [main] l.n.CompleteTest1 - 1
// future.complete("1"); // log.info(base.get()); 输出:一直阻塞
这里base和future对象,分别调用complete()和get()方法的排列组合,这四种组合的结果是完全不一样的。
数据结构:
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> { // ...... volatile Object result; // Either the result or boxed AltResult volatile Completion stack; // Top of Treiber stack of dependent actions }
CompletableFuture有两个关键成员属性,一个是Completion对象stack,这是一个CAS实现的无锁并发栈,每个链式调用的任务会被压入这个栈。另一个是Object对象result,这是当前CompletableFuture的结果。
abstract static class Completion extends ForkJoinTask<Void> implements Runnable, AsynchronousCompletionTask { }
abstract static class UniCompletion<T,V> extends Completion { Executor executor; // executor to use (null if none) CompletableFuture<V> dep; // the dependent to complete CompletableFuture<T> src; // source for action UniCompletion(Executor executor, CompletableFuture<V> dep, CompletableFuture<T> src) { this.executor = executor; this.dep = dep; this.src = src; } }
其实每次调用都会new一个Completion对象,并压入上一个CompletableFuture的stack中。所以,通常的base.thenApply(..).thenApply(..),每次调用产生的Completion并不在同一个stack中。
链式调用如何传递?
public boolean complete(T value) { boolean triggered = completeValue(value); // 在上一步值获取到之后,触发下游依赖执行相应方法。 postComplete(); return triggered; }