【JUC】CompletableFuture详解

Future接口(Java 5新增)

Future接口定义了操作异步任务 的一些方法(如获取异步任务的执行结果、取消任务、判断任务是否被取消/完成等),提供了一种异步并行计算的功能,其实就是定义了一些规范,来保证异步任务执行的状态反馈 +获取任务执行的结果

举个🌰:主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就可以去做其他事情,过了一会获取子任务的执行结果或者变更的任务状态(是否有返回值 ,是否抛出异常等)

总结下出现的原因+作用:

Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务

FutureTask(Future接口的常见实现类)

还记得上文中举的例子吗?主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就可以去做其他事情,过了一会获取子任务的执行结果或者变更的任务状态(是否有返回值 ,是否抛出异常等)。

我们拿生活中的一个非常常见的场景来打个比方:小明要做咖喱牛肉饭,他需要蒸一份米饭+炒一份咖喱牛肉,于是小明把生米准备好放进电饭锅,选择煮饭模式,接着他就去洗菜切菜炒菜了,在菜炒快炒好的时候,电饭锅"滴"了一声,提示他提示米饭🍚已蒸好。

在这个场景下,我们就需要的实现类至少要满足3个条件:

  • 多线程(另外的电饭锅煮饭而不是都用一个炒菜锅)
  • 能获取返回值(饭是否蒸好了)
  • 异步(炒菜的同时也在蒸饭)

接下来我们就来看看我们的FutureTask

可以看到FutureTask实现了RunnableFuture接口,并且构造方法中可以接收实现了Callable接口的变量(有返回值),因此FutureTask是符合这三个条件的

本源的Future接口相关架构

FutureTask<V>类实现了RunnableFuture<V>接口

RunnableFuture<V> 接口继承了Runnable接口和 Future<V>接口

因此它们的关系如下:

FutureTask<V>类有两个构造方法,一个需要传Callcable<V>类型的参数,一个需要传Runnable类型的参数

优点 vs. 缺点

首先,一个很直接的优点就是:FutureTask+线程池异步多线程任务配合,能显著提高程序的执行效率。其次,它也有一些缺点:

调用get()可能会阻塞

调用get()就会马上去拿结果,如果立马拿不到结果就会阻塞(未计算完成会阻塞主线程),可以通过加超时设置来使用(但不优雅= =)

isDone()轮询可能导致CPU空转

轮询的方式会耗费CPU资源(并且不优雅= =/),而且也不一定能及时的得到计算结果(但优于可能会导致阻塞的方式)

因此我们可以得到结论:FutureTask对结果的获取不是十分的友好(要么轮训要么阻塞),因此对于简单的业务场景使用FutureTask一般是没有什么问题的,但是如果场景复杂起来,比如需要一堆异步计算的结果值进行叠加,后一个异步计算需要依赖前一个异步计算的结果值等等,使用FutureTask的那些API就不那么方便了,于是我们有了CompletableFuture

CompletableFuture对Future的改进

FutureTaskget()方法会导致阻塞(这与异步编程的设计理念相违背),而isDone()方法耗费CPU,其实比较理想的方式是通过传入回调函数,在FutureTask执行完成时自动调用该回调函数,这样就不用等待结果。

于是,JDK8出现了ComplateblFutureComplateblFuture实现了CompletionStageFuture,提供了非常强大的Future的扩展功能,可以极大的简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合的方法,它可能代表一个明确完成的Future,也可能代表一个完成阶段的CompletionStage,它支持在计算完以后触发一些函数或者执行某些动作。一句话概括下:FutureTask能干的CompletableFuture都能干(完全能取代Future接口),而且CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方,更加优雅。

从源码中可以看到CompletableFuture实现了CompletionStageFuture接口

CompletionStage接口

CompletionStage代表异步计算过程中的某一个阶段(一个阶段完成后可能会触发另一个阶段)

如何创建一个异步任务

官方并不建议直接new一个CompletableFuture,因为这样创建出来的CompletableFuture是不完整的

那么,我们该如何创建一个CompletableFuture呢?

4大核心静态方法

建议使用如下4个静态方法进行获取

其中runAsync()是无返回值的,而supplyAsync()是有返回值的

对于无返回值的runAsync(),如果传入了线程池对象,那么就会使用传入的线程池,否则就使用默认的ForkJoinPool线程池

同样的,对于有返回值的supplyAsync(),如果传入了线程池对象,那么就会使用传入的线程池,否则就使用默认的ForkJoinPool线程池

CompletableFuture的优点

异步任务结束时,会自动回调指定的方法,主线程社设置好回调以后,不用再去关注异步任务的执行,异步任务之间可以顺序执行,异步任务出错❌时,会自动调用指定的方法,减少了阻塞和轮询,是Future的增强版!

CompletableFuture常用方法

获得结果和触发计算

join():作用和get()一致,不过不抛出异常

getNow():折中处理,没有立刻计算完成,提供"备胎值";

complete():不抛异常,不阻塞,true时返回计算值;false时返回括号内的值

对计算结果进行处理

thanApply():计算结果存在依赖关系(串行化),前一步有异常就会"叫停",不会走后一步

默认线程池(ForkJoinPool)会在主线程结束后立刻关闭,建议自定义线程池

handle:计算结果存在依赖关系(串行化),前一步有异常依旧会走后一步

对计算结果进行消费

thanAccept():接收任务的处理结果并消费处理,无返回值

对比补充(任务之间的顺序执行)
less 复制代码
System.out.println(CompletableFuture.supplyAsync(() -> "hehe~").thenRun(() -> {}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "hehehe~").thenAccept(r -> System.out.println(r)).join());
System.out.println(CompletableFuture.supplyAsync(() -> "hehehehe~").thenApply(r -> r + "dadada~").join());

对计算结果进行选用

applyToEither():谁快用谁

对计算结果进行合并

thenCombine()

相关推荐
真是他几秒前
多继承出现的菱形继承问题
后端
Java技术小馆几秒前
SpringBoot中暗藏的设计模式
java·面试·架构
xiguolangzi1 分钟前
《springBoot3 中使用redis》
java
李菠菜4 分钟前
POST请求的三种编码及SpringBoot处理详解
spring boot·后端
李菠菜5 分钟前
浅谈Maven依赖传递中的optional和provided
后端·maven
李菠菜8 分钟前
非SpringBoot环境下Jedis集群操作Redis实战指南
java·redis
lqstyle9 分钟前
Redis的Set:你以为我是青铜?其实我是百变星君!
后端·面试
Piper蛋窝12 分钟前
Go 1.15 相比 Go 1.14 有哪些值得注意的改动?
后端
K8sCat19 分钟前
Golang与Kafka的五大核心设计模式
后端·kafka·go
不当菜虚困21 分钟前
JAVA设计模式——(四)门面模式
java·开发语言·设计模式