异步函数式编程利器 CompletableFuture

前言

最近在领导那学到一个非常好用的异步函数式编程工具 CompletableFuture ,看下源码发现居然是 Java8 提供的类,惭愧至极,居然现在才知道。此类功能非常全面强大,提供了简单的链式调用函数实现任务的串行、并行、组合等等。本篇文章和大家一起学习 CompletableFuture

CompletableFuture 简介

在介绍 CompletableFuture 之前让我们来思考一个问题,假如你现在要约你的女朋友小樱出去看电影,我们把这件事看成一个 Task。思考一下完成这个 Task 可能会经过哪些阶段?预期的场景可能是首先要发微信或者打电话联系,然后小樱同意了、小樱开始洗澡、小樱化妆、你在买票、小樱出门、你也出门、小樱乘坐交通工具到达约定地点......最终你们幸福的坐在电影院看电影

image.png

简易的阶段图可能是这样,还有未考虑到的 task,你会发现一个 Task 可以拆分为多个小的 task,同时你还会发现在多个小的 task 里面有些 task 是有依赖关系的,比如小樱洗澡和小樱化妆,也有些 task 是可以同时执行的,比如小樱洗澡和你买电影票,这是两个没有依赖关系的 task

有了上面的生活场景,我们再来理解 CompletableFuture 就简单的多了,CompletableFutureJava8 提供,Java9增强的一个改进 FutureTask实现异步任务的类,除了 Future 接口,它还实现了 CompletionStage 接口,它的设计思想就是每一个任务都可以拆分为几个子任务,每一个子任务的执行都可以抽象成一个完成阶段

image.png

当这个过程中任何阶段出现异常时,我们都可以对其进行回调处理,同时异常处理过程也是一个阶段,所以在处理完异常后我们仍然可以开启一个新的 CompletableStageCompletableFuture 用这种思想提供了一系列链式方法,实现了任务可以任意串行、并行、组合各种操作。一个阶段完成后可以主动触发下一个阶段,这是 CompletableFuture 的核心优势。

相比 FutureTask

相比之前用的 FutureTaskCompletableFuture 提供的明显优势有以下几点:

  • FutureTask 获取异步任务的执行结果时,要么调用 get() 阻塞,要么轮询 isDone()主动查询是否完成,这两种方法都不太好,会使主线程被迫等待。 而 CompletableFuture 完成一个任务阶段之后可以主动通知下一个阶段

  • FutureTask 没有提供异常处理的 API ,而 CompletableFuture 对于每一个阶段都提供了异常回调 exceptionally()

  • FutureTask 无法创建一个任务的工作流,而 CompletableFuture 可以创建一个包含任意数量 CompletionStage 的工作流

  • CompletableFuture 可以将多个任务阶段任意组合、串行、并行,以简单的 API 组合,实现复杂的操作

如果你还不了解 FutureTask 的话,那么恭喜你,直接学习 CompletableFuture 吧,如果你对 FutureTask 略微了解,那么赶紧忘了它~~让我们愉快的学习 CompletableFuture

CompletableFuture 用法

下面简介 CompletableFuture 提供的 API 用法,在学习 CompletableFuture 之前我希望大家能够对 Java8lambda 和几个函数式接口例如 Function<T,R>、Consumer<T>、Supplier<T>、BiFunction<T,U,R> 等能够熟练使用,因为 CompletableFuture 提供的重要 API 入参几乎全都是函数式接口。

提交异步任务

使用下面这段代码来提交一个简单的异步任务

CompletableFuture.supplyAsync(() -> {
    System.out.println("任务执行中");
    return "任务结果";
});
复制代码

不过有意思的是你会发现 main 方法中这个入门案例在控制台很大概率是没有任何打印,我在学习的时候给我困惑了一会......后来灵机一动发现,莫非是主线程结束导致的?思考一下如果是的话说明了什么?

守护线程

说明使用 CompletableFuture 执行任务的线程是守护线程啊!我们都知道主线程是用户线程,当所有用户线程结束,JVM 进程就自动退出了。所以我们可以猜测 CompletableFuture 提交任务的处理线程是守护线程, debug 验证一下,在 supplyAsync() 中查看当前线程的 daemo 属性的确是 true,这说明它和垃圾回收线程一样是守护线程。

下面的代码示例中我将省略主线程休眠代码 TimeUnit.SECONDS.sleep(2);

获取异步任务的结果

我们可以使用 get()join() 获取异步任务的结果,两者的区别是 join() 不需要你强制性的捕捉或上抛异常。不过他们都是阻塞方法

CompletableFuture future = CompletableFuture.supplyAsync(() -> {
    //执行任务...
    return "任务结果";
});
System.out.println(future.join());//获取任务结果
复制代码

看到这你可能会发现,这不还是和 FutureTask 一样获取任务结果会阻塞吗?别急呀,还有很多性感姿势呢~~

任务依赖

当我们需要执行两个有依赖关系的任务,即在一个异步任务完成后能够收到其通知,携带着它的执行结果来执行另一个异步任务,那么我们可以使用 thenCompose 方法来实现

CompletableFuture.supplyAsync(() -> {
    System.out.println("我媳妇儿正在炒菜");
    return "红烧排骨、清蒸鲈鱼";
}).thenCompose(dish -> CompletableFuture.supplyAsync(() -> {  //接着上一个任务结束之后,开启一个异步任务
    System.out.println("媳妇儿通知我吃她做好的菜:"+dish);
    return null;
}));
复制代码

看到没有,第一个异步任务的结果是不是不需要阻塞获取结果或者循环判断是否完成?

任务合并

有时候我们希望在两个异步任务都完成后进行下一阶段的操作,此时我们可以使用 thenCombine 合并两个没有依赖关系的任务并行执行,当两个任务都完成之后对其结果进行操作。

CompletableFuture.supplyAsync(() -> {
    System.out.println("我女朋友小樱正在做红烧排骨");
    return "红烧排骨";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
    System.out.println("我女朋友小葵正在做清蒸鲈鱼");
    return "清蒸鲈鱼";
}),(t1,t2) -> { //两个任务都执行完之后才会执行
    System.out.println(t1+","+t2+" 并行做好了,通知我开吃!!");
    return "";
});
复制代码

那你可能已经发现问题了,如果说我有两个以上没有依赖关系的任务需要并行执行呢?当然 CompletableFuture 也给我们提供了一个 allOf(CompletableFuture<?>... cfs) 方法,此方法可以处理任意多个并行任务。

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1完成");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2完成");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "任务3完成");

CompletableFuture.allOf(future1,future2,future3).thenAccept(t -> {
    //所有任务完成之后...
    System.out.println("所有任务完成了,接下来...");
});
复制代码

任务选择

有时候我们希望两个异步任务任意一个完成即可进行下一阶段的操作,那么可以使用 applyToEither 来实现

CompletableFuture<String> bus = CompletableFuture.supplyAsync(() -> {
            System.out.println("我约女朋友小樱出来看电影");
            System.out.println("女朋友小樱正在洗澡、化妆...");
            return "女朋友小樱准备好了";
        }).applyToEither(CompletableFuture.supplyAsync(() -> {
            System.out.println("我约女朋友小葵出来看电影");
            System.out.println("女朋友小葵正在洗澡、化妆...");
            return "女朋友小葵准备好了";
        }), firstReady -> { //先执行完的任务结果
            System.out.println(firstReady+",我们一起去看电影");
            return firstReady;
        });
复制代码

那什么我要狡辩一下啊......像我这么怜香惜玉的暖男正常情况下是不会放妹子鸽子的,这里只是示例需要啊......通常我遇到这种情况的话都是使用 thenCombine

那么问题又来了,类似 allOf()CompletableFuture 也提供了 anyOf(CompletableFuture<?>... cfs) 方法来实现多个并行任务中任意一个完成即可进行下一阶段操作。

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1完成");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2完成");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "任务3完成");

CompletableFuture.anyOf(future1,future2,future3).thenAccept(t -> {
    //任意任务完成...
    System.out.println("有一个任务完成了,接下来...");
});
复制代码

异常处理

没有人能保证自己的代码不出错,exceptionally 该方法用于在一个或几个阶段后调用,处理前几个阶段可能会出现的异常,值得注意的是如果你不调用 exceptionally,并且也没有显示获取异步任务的结果,那么异步任务中出现异常时将不会展现异常信息,这是非常值得注意的地方。我就是因为这个捕捉了领导的一个 bug

   CompletableFuture.supplyAsync(() -> {
            System.out.println("女朋友小樱正在做饭...");
            if (true) {
                throw new RuntimeException("厨房着火了...");
            }
            return null;
        }).thenApply((result) -> {
            System.out.println("等待女朋友叫我吃饭...");
            return string;
        });
复制代码

这种情况下 supplyAsync 一旦出现异常,控制台输出 女朋友小樱正在做饭... 之后没有就任何反应了,试问一个刚接触这玩意的人,程序不报错但是也没成功达到逾期,这真的让人头大呀,当时弄了好久才发现是这个原因......此时我心里想对领导说:你丫的干啥呢,搞这么大一个 bug,害我浪费半天时间 !哈哈,开个玩笑,推荐大家使用时,对于重要的阶段一定要调用此方法或者手动 try-catch 以免出现错误无法捕捉。

当然如果你就是不想调用 exceptionally() 也不想使用 try-catch,那么对于如此任性的你 CompletableFuture 还提供了两个 API handle、whenComplete ,无论任务正常结束还是异常结束都会回调这两个方法。 以 handle 为例

CompletableFuture.supplyAsync(() -> {
    System.out.println("女朋友小樱正在做饭");
    if (true) { //模拟异常
        throw new RuntimeException("厨房着火了...");
    }
    return "小樱做好了午餐";
}).handle((s, e) -> {
    if(e == null){
        //没有异常,说明小樱做好了午餐
        System.out.println(s);
    } else {
        //有异常,说明小樱做饭过程中出现了异常
        System.out.println(e);
    }
    return null;
});
复制代码

但是很明显这样看起来怪怪的......还得判断异常对象是不是 null 。所以还是使用 exceptionally() 吧!

如出一辙

如果你仔细学习 CompletableFuture API的话,你会发现它有很多很多,乍一看挺吓人的,但其实很多功能都是几乎一样的,只有略微的差别,我们可以将其归类。例如

  • thenApply、thenAccept、thenRun
  • thenCombine、thenAcceptBoth、runAfterBoth
  • applyToEither、acceptEither、runAfterEither

上面每一组三个方法之间都是几乎一样的功能,区别是第一个 API 需要接受上一个阶段的返回值作为参数,并且执行完毕会有返回值。第二个 API 是需要接受前一阶段的返回值作为参数,但是没有返回值。第三个 API 是既不需要前一阶段的返回值作为参数也没有返回值。所以可以根据自己实际的需求选择使用适合的方法。

  • applyToEither、anyOf
  • thenCombine、allOf

上面每一组两个方法的区别是,第一个 API 用于两个异步任务,而第二个可以用在多个任务上,也就是说 applyToEither、thenCombine 可以实现的,我们当然也可以使用 anyOf、allOf 实现。

还有很多其他 API 我就不一一介绍了......再介绍就变成 API 文档了,总之自己多用、多尝试、多看注释、多看方法形参和返回值就可以啦~

进阶组合

前面我们学习了 任务合并任务依赖任务选择 三种基本而又核心使用,我相信聪明的你已经开始思考三种组合的场景了,其实很简单,只需要合理调用 API 即可轻松实现

image.png

例如上图我需要选择 Task1 和它上面那一堆两者先执行完毕的结果去执行中 Task7,在 Task1 上面那一堆中又合并了 Task2Task3、Task4、Task5。其中 Task3 → Task4 → Task5 又是依赖关系。 当然这是一个很简单的组合任务,任何复杂的组合也都可以使用 CompletableFuture 实现。

再把上图的所有 Task 都想象成内部还包含一个上图的组合,脑袋是不是不够用了?只有你想不到的,没有它做不到的!

结语

本篇文章简单介绍了 CompletableFuture 的基本设计思想和基本使用方法。不得不说它是真的实现了较为完美的异步编程,其方法入参更是使用了各种函数式接口来传递行为和提升抽象层次。下篇文章将会介绍 CompletableFuture 进阶知识,关于线程池和性能相关。

如果这篇文章对你有帮助,记得点赞加关注。你的支持就是我继续创作的动力!

おすすめ

転載: juejin.im/post/7039718229115158536
おすすめ