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的改进
FutureTask
的get()
方法会导致阻塞(这与异步编程的设计理念相违背),而isDone()
方法耗费CPU,其实比较理想的方式是通过传入回调函数,在FutureTask
执行完成时自动调用该回调函数,这样就不用等待结果。
于是,JDK8出现了ComplateblFuture
,ComplateblFuture
实现了CompletionStage
和Future
,提供了非常强大的Future
的扩展功能,可以极大的简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合的方法,它可能代表一个明确完成的Future
,也可能代表一个完成阶段的CompletionStage
,它支持在计算完以后触发一些函数或者执行某些动作。一句话概括下:FutureTask
能干的CompletableFuture
都能干(完全能取代Future
接口),而且CompletableFuture
提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方,更加优雅。
从源码中可以看到CompletableFuture
实现了CompletionStage
和Future
接口
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()