【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()

相关推荐
草莓base2 分钟前
【手写一个spring】spring源码的简单实现--bean对象的创建
java·spring·rpc
Estar.Lee4 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
drebander26 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
乌啼霜满天24929 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
tangliang_cn35 分钟前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟35 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
Grey_fantasy1 小时前
高级编程之结构化代码
java·spring boot·spring cloud
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
弗锐土豆1 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
Elaine2023911 小时前
零碎04 MybatisPlus自定义模版生成代码
java·spring·mybatis