CompletableFuture

一、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;
}

七、参考

王者并发课-钻石03:琳琅满目-细数CompletableFuture的那些花式玩法 - 掘金7192ea2

深入理解JDK8新特性CompletableFuture-阿里云开发者社区

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

M.Rambo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值