一文秒懂 Java CompletableFuture

在Java 8中,新增了许多非常有趣的功能,比如Labmda表达式、流式API(Stream API)等。这些语法糖或API让Java这门“古老”的编程语言也开始具备“现代”的气息。但是,在所有这些新功能中最让笔者觉得耳目一新,并认为其将Java平台提高到一个新境界的功能是异步执行框架CompletableFuture。

在Java 8之前,我们都知道用于异步执行的ExecutorService类和代表异步执行结果的Future类。Future类提供了一个get方法,用于在任务完成时获取任务的结果。但是,Future类的get方法有个缺点,它是阻塞的。具体而言就是,虽然我们将任务提交给ExecutorService异步执行了,但还是需要使用get方法来同步等待任务结果。这就在事实上将原本异步执行的方案,重新退化成了同步执行,失去了原本异步方案的意义。

 图1 submit和execute的执行原理

为了避免这种问题,不同的第三方Java库或框架提供了不同的解决方案,比如Guava库中的SettableFuture/ListenableFuture、Netty中的Future和ChannelFuture等。这些解决方案都是通过注册监听或回调的方式,形成回调链,从而实现了真正意义上的异步执行。

在借鉴了诸多第三方异步编程方案后,Java 8带来了自己的异步编程方案CompletableFuture类。CompletableFuture类也是采用了回调的方式实现异步执行,但除了提供基本的回调执行机制外,CompletableFuture类提供了大量有关异步回调链构造的API,这些API使得Java异步编程变得无比灵活和方便,极大程度地解放了Java异步编程的生产力。

下面是我们在日常开发中常用的一些CompletableFuture类方法。

(1)将产品放到流水线起点上


public static <U> CompletableFuture<U> supplyAsync(
    Supplier<U> supplier, Executor executor)

CompletableFuture.supplyAsync是开启CompletableFuture异步调用链的方法之一。使用这个方法,会将supplier封装为一个任务提交给executor执行,并返回一个记录这个任务执行状态和执行结果的CompletableFuture对象。之后可以在这个CompletableFuture对象上挂接各种回调动作。所以说它是流水线的起点,将产品原料放在了流水线上。

(2)产品在流水线上的加工


public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn, Executor executor)

thenApplyAsync用于在CompletableFuture对象上挂接一个转化函数。当CompletableFuture对象完成时,以其结果作为输入参数调用转化函数。转化函数内部在执行各种逻辑后,返回另一种类型的数据作为输出。该方法的返回是一个新的CompletableFuture对象,用于记录转化函数的执行状态和执行结果等信息。thenApplyAsync的fn参数将一种类型数据转化为另外一种类型数据,就像流水线上生产工人对半成产品加工处理的过程。

(3)产品在流水线上完成加工后装入仓库


public CompletableFuture<Void> thenAcceptAsync(
    Consumer<? super T> action, Executor executor)

thenAcceptAsync用于在CompletableFuture对象上挂接一个接收函数。当CompletableFuture对象完成时,以其结果作为输入参数调用接收函数。与thenApplyAsync类似,接收函数在其内部可以执行各种逻辑,但不同的是,接收函数不会返回任何类型数据,或者说返回类型是void。因此,thenAcceptAsync通常就是接收并消化任务链的最终输出结果。这就像产品在流水线上完成所有加工后,从流水线上拿下来装进仓库的过程。

(4)在流水线上插入一条新流水线


public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

thenComposeAsync理解起来会复杂些,但它真的是一个非常有用的方法。thenComposeAsync在API形式上与thenApplyAsync类似,但是它的转化函数返回的不是一般类型的对象,而是一个CompletionStage对象,或者说得更具体点,实际使用中通常就是一个CompletableFuture对象。这意味着,我们可以在原来的CompletableFuture调用链上,插入另外一个调用链,从而形成一个新的调用链。这正是compose(组成、构成)的含义所在。而这个过程,就像是在流水线的某个环节处,插入了另外一条流水线。不过需要注意的是,“插入”这个词带有“已有”和“原来”的意味,但是实际在程序设计和开发时,并非一定是对旧物的改造,而是说如果某个步骤内部有另外的异步执行过程,可以直接将这条独立的异步调用链加入到当前调用链中来,成为整体调用链的一部分。

(5)谁先完成谁执行


public <U> CompletableFuture<U> applyToEither(
    CompletionStage<? extends T> other, Function<? super T, U> fn)

使用applyToEither可以实现两个CompletableFuture谁先完成就由谁执行回调函数的功能。比如,可以用该方法实现定时超期的功能。具体而言就是,用一个CompletableFuture表示目标任务,用另外一个CompletableFuture表示定时任务,这样如果目标任务在定时任务完成前尚未完成,就由定时任务做善后处理。这里只是列举了一个使用场景,读者可根据自己的需要任意发挥applyToEither的用法。

(6)大家一起完成后再执行


public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

CompletableFuture.allOf的功能是将多个CompletableFuture合并成一个CompletableFuture。这又是一个非常有用而且有趣的方法,因为我们可以用它实现类似于Map/Reduce或Fork/Join的功能。在多核和并行计算大行其道的今天,诸如Map/Reduce或Fork/Join这类先分散再汇聚的执行流结构是非常普遍的,CompletableFuture.allOf为我们编写这类模式的执行逻辑提供了非常方便的方法。在下一节中,我们就会看到这个方法的实际案例。

(7)异常处理


public CompletableFuture<T> exceptionally(
        Function<Throwable, ? extends T> fn)

在Java的世界里,异常无处不在。在CompletableFuture中发生异常了会怎样?实际上,如果不是CompletableFuture提供了exceptionally等异常处理方法,而是由我们自己在回调函数里做异常处理的话,会非常受限和不方便。稍有不注意,就会写出不合理甚至错误的代码。比如你认为捕获了的异常,实际上根本就不是在那个地方或那个线程上抛出。出现这种情况的原因在于,在异步的世界里,即使是同一份代码,实际上在运行起来后,其调用链生成、回调的执行时刻、回调所在线程和回调的上下文环境都是灵活多变的。相比以前同步或半异步半同步的编程方式,使用CompletableFuture开发的程序在运行时的状况会更加复杂和多变。而CompletableFuture的exceptionally方法就为我们提供了相对较好的异常处理方案。使用exceptionally方法,可以对指定CompletableFuture抛出的异常进行处理。比如捕获异常并返回一个特定的值,或者继续将异常抛出。

下面我们来分析下CompletableFuture的内部工作原理。考虑下面的实验程序片段:


CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Tests::source, executor1);
CompletableFuture<String> cf2 = cf1.thenApplyAsync(Tests::echo, executor2);
CompletableFuture<String> cf3_1 = cf2.thenApplyAsync(Tests::echo1, executor3);
CompletableFuture<String> cf3_2 = cf2.thenApplyAsync(Tests::echo2, executor3);
CompletableFuture<String> cf3_3 = cf2.thenApplyAsync(Tests::echo3, executor3);
CompletableFuture<Void> cf3 = CompletableFuture.allOf(cf3_1, cf3_2, cf3_3);
CompletableFuture<Void> cf4 = cf3.thenAcceptAsync(x -> print("world"), executor4);

调试跟踪并分析以上实验程序,其工作原理如图2所示。


图2  CompletableFuture执行过程

图2描述了实验程序整体的执行过程:

1)通过CompletableFuture.supplyAsync创建了一个任务Tests::source,并交由executor1异步执行。用cf1来记录该任务在执行过程中的状态和结果等信息。

2)通过cf1.thenApplyAsync,指定了当cf1(Tests::source)完成时,需要回调的任务Tests::echo。cf1使用stack来管理这个后续要回调的任务。与cf1类似,用cf2来记录任务Tests::echo的执行状态和执行结果等信息。

3)通过连续三次调用cf2.thenApplyAsync,指定了当cf2(Tests::echo)完成时,需要回调后续三个任务:Tests::echo1、Tests::echo2和Tests::echo3。与cf1一样,cf2也是用stack来管理其后续要执行的这三个任务。

4)通过CompletableFuture.allOf,创建一个合并了cf3_1、cf3_2、cf3_3的cf3,cf3只有在其合并的所有cf完成时才能完成。在cf3内部,是用一个二叉树(tree)来记录其和cf3_1、cf3_2、cf3_3的依赖关系。这点后续会详细描述。

5)通过cf3.thenAcceptAsync,指定了当cf3完成时,需要回调的任务(print)。用cf4来记录print任务的状态和结果等信息。

总结起来就是:

1)CompletableFuture用stack来管理其在完成(complete)时后续需要回调的任务(Completion)。

2)在AsyncRun、Completion中,通过依赖(dep)指针,指向后续需要处理的CompletableFuture,这样在任务完成后,就可以通过dep指针找到后续处理的CompletableFuture,从而继续执行。

通过1)和2)形成一个调用链,所有任务按照调用链执行。

 图3 CompletableFuture工作原理

图3描述了CompletableFuture链是如何组织和执行的。总的来说,每个CompletableFuture可以存在三种类型的指针:src、snd和dep。其中dep指向了这个CompletableFuture在完成(completed)时,后续继续调用的CompletableFuture。src和snd则指向了其链接的另外的两个CompletableFuture,用于决定是否在CompletableFuture完成时触发dep执行。CompletableFuture内部就是用这三个指针巧妙地管理CompletableFuture之间各种复杂的依赖和调用关系的。对于每个CompletableFuture节点,当其被触发执行时,如果其src和snd(如果存在snd)都是completed状态(src或snd指向自己时也算completed状态),就触发其dep执行,否则就不触发其dep执行。但不管这个CompletableFuture是否触发了其dep执行,在tryFire(ASYNC)过后,这个CompletableFuture本身就是已经completed的了。如果它没有触发dep,就会由该CompletableFuture的src或snd在被触发时按照同样的方式做处理。

作者简介:周爽,本硕毕业于华中科技大学,先后在华为2012实验室高斯部门和上海行邑信息科技有限公司工作。开发过实时分析型内存数据库RTANA、华为公有云RDS服务、移动反欺诈MoFA等产品。目前但任公司技术部架构师一职。著有《实时流计算系统设计与实现》

 

推荐语:透过现象看本质,掌握高性能、高并发、实时系统设计与权衡之道!

这本书高度抽象出实时流计算系统的技术支撑、架构模式、编程模式、系统实现与协同系统,并从零编写一个分布式实时流计算系统。多位领域专家联袂推荐!

读书日签



更多精彩回顾



书讯 | 6月书讯 (上)| 初夏已至,书香有约,六月宜静心读书书讯 | 6月书讯 (下)| 初夏已至,书香有约,六月宜静心读书上新 | 周志华领衔撰写,历时4年,宝箱书问世!
书单 | 创建字节跳动之前,张一鸣读过哪些硬核技术书?干货 | G1垃圾回收算法概述收藏 | TIOBE 5月榜单:时隔五年,C语言重返第一

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值