本文详细解释了java8中CompletableFuture的特性、方法和示例。
在java8之前,我们使用java的多线程编程,这通常是通过Runnable中的run方法来完成的。这种方法有一个明显的缺点,就是没有返回值。这时候大家可以尝试使用Callable中的call方法,然后用Future返回结果,如下:
通过观察控制台,我们发现先打印主线程,一秒钟后打印异步线程看起来是符合我们的需求的,但是仔细考虑之后发现了一个问题。调用future的get()方法时,当前主线程被阻塞,这似乎不是我们想看到的。另一种得到返回结果的方法是先轮询,可以叫isDone,但这并不能让我们满意。
无论如何,这种用法看起来并不优雅,至少从视觉,来看很难看,有些场景是不能用的,比如,
1.很多异步线程的执行时间可能不一致,所以我的主线程业务不能一直等下去。此时,我可能想等待最快的线程完成执行或最重要的任务完成执行,或者我只等待1秒钟。至于没有返回结果的线程,我将改用默认值。
2.执行在我的两个异步任务之间是独立的,但是第二个任务依赖于第一个任务的执行结果。
java8的CompletableFuture已经在这个混乱不完善的多线程江湖中出道了。CompletableFuture极大地改进和扩展了Future的功能和使用场景,提供了函数式编程能力,并使代码更加美观和优雅。此外,处理结果可以通过回调来计算,对于异常的处理有更好的处理方法
在执行异步任务:的完整的未来源代码中有四个静态方法
[
[
如果有多线程的基础知识,我们很容易看到,run开始时的两个方法是用于执行没有返回值的任务,因为它的输入是Runnable对象,而supply开始时的方法显然是执行有返回值的任务。如果执行器对象没有传入,那么ForkJoinPool.commonPool()将被用作它的线程池执行异步代码。在实际使用中,我们通常使用自己的线程池对象作为参数传入,这样比较快。
执行的异步任务模式也很简单,只需要用上面的方法就可以:
接下来,让我们看看获得执行结果的几种方法。
以上两种方法在Future中实现,get()会阻塞当前线程,造成问题。如果执行线程长时间不返回数据,get()将一直等待,因此第二个get()方法可以设置等待时间。
getNow()方法更有意思,就是返回结果的时候会返回结果,如果异步线程抛出异常,会返回自己设置的默认值。
接下来,我们将通过一些场景示例介绍CompletableFuture中其他一些常用的方法。
功能:在执行,正常完成当前任务后,当前任务的执行结果可以作为下一个任务的输入参数,没有返回值。
场景:执行任务A和执行任务B异步,任务B正常返回后,执行任务C返回值为B,任务C没有返回值
[
[
功能:不关心上一步的计算结果,执行下一步操作
场景:执行任务A,执行,执行任务B之后,任务B不接受任务A的返回值(不管A有没有返回值),没有返回值
功能:当前任务正常完成后,执行,当前任务的执行结果将作为下一个任务的输入参数,带有返回值
场景:任务与执行,和执行串联,下一个任务取决于前一个任务的结果,并且每个任务都有输入和输出
例1:异步执行任务a,当任务a完成时,用任务a的返回结果a作为输入,执行任务b的处理,可以实现任意数量任务的串行执行
[
[
有了上面的代码,当然可以调用future.join()先得到任务A的返回值,然后把返回值作为执行任务b的输入,应用的存在就是帮我简化这一步。我们不必因为等待计算完成而一直阻塞调用线程,而是告诉CompletableFuture在执行完成时继续下一步,并串联多个任务。
[
[
功能:合并了两个CompletionStage的结果,并在转换后返回
场景:需要根据商品id查询商品的当前价格。查询商品的原价格和折扣分两步。这两个查询相互独立。当两者都被发现时,原始的价格乘以折扣来计算当前的价格使用方法:
[
[
然后组合(…)将两个任务的返回值组合起来,然后返回它们。如果不需要返回,那么就需要nacceptbit (…)。同样,如果您不关心两个任务的返回值,您需要在两者之后运行。如果你理解了以上三种方法,然后应用,然后接受,然后运行,这里就不需要分别提到这两种方法了。
功能:这个方法接收的输入是当前的Complet
ableFuture的计算值,返回结果将是一个新的CompletableFuture这个方法和thenApply非常像,都是接受上一个任务的结果作为入参,执行自己的操作,然后返回.那具体有什么区别呢?
thenApply():它的功能相当于将CompletableFuture转换成CompletableFuture,改变的是同一个*CompletableFuture中的泛型类型*
*thenCompose():用来连接两个CompletableFuture,返回值是一个新的CompletableFuture*
[
[
这段代码实现的和上面thenApply一样的效果,在实际使用中,我并没有很清楚两个在使用上的区别,如果有大佬,跪求告知.
功能:执行两个CompletionStage的结果,那个先执行完了,就是用哪个的返回值进行下一步操作
场景:假设查询商品a,有两种方式,A和B,但是A和B的执行速度不一样,我们希望哪个先返回就用那个的返回值.
[
[
同样的道理,applyToEither的兄弟方法还有acceptEither(),runAfterEither(),我想不需要我解释你也知道该怎么用了.
功能:当运行出现异常时,调用该方法可进行一些补偿操作,如设置默认值.
场景:异步执行任务A获取结果,如果任务A执行过程中抛出异常,则使用默认值100返回.
[
[
上面代码展示了正常流程和出现异常的情况,可以理解成catch,根据返回值可以体会下.
功能:当CompletableFuture的计算结果完成,或者抛出异常的时候,都可以进入whenComplete方法执行,举个栗子
[
[
根据控制台,我们可以看出执行流程是这样,supplyAsync->whenComplete->exceptionally,可以看出并没有进入thenApply执行,原因也显而易见,在supplyAsync中出现了异常,thenApply只有当正常返回时才会去执行.而whenComplete不管是否正常执行,还要注意一点,whenComplete是没有返回值的.
上面代码我们使用了函数式的编程风格并且先调用whenComplete再调用exceptionally,如果我们先调用exceptionally,再调用whenComplete会发生什么呢,我们看一下:
[
[
代码先执行了exceptionally后执行whenComplete,可以发现,由于在exceptionally中对异常进行了处理,并返回了默认值,whenComplete中接收到的结果是一个正常的结果,被exceptionally美化过的结果,这一点需要留意一下.
功能:当CompletableFuture的计算结果完成,或者抛出异常的时候,可以通过handle方法对结果进行处理
[
[
通过控制台,我们可以看出,最后打印的是handle result:futureA result: 100,执行exceptionally后对异常进行了"美化",返回了默认值,那么handle得到的就是一个正常的返回,我们再试下,先调用handle再调用exceptionally的情况.
[
[
根据控制台输出,可以看到先执行handle,打印了异常信息,并对接过设置了默认值500,exceptionally并没有执行,因为它得到的是handle返回给它的值,由此我们大概推测handle和whenComplete的区别
1.都是对结果进行处理,handle有返回值,whenComplete没有返回值
2.由于1的存在,使得handle多了一个特性,可在handle里实现exceptionally的功能
allOf:当所有的都执行完后执行计算
anyOf:最快的那个CompletableFuture执行完之后执行计算
场景二:查询一个商品详情,需要分别去查商品信息,卖家信息,库存信息,订单信息等,这些查询相互独立,在不同的服务上,假设每个查询都需要一到两秒钟,要求总体查询时间小于2秒.
[
[
参考资料:
https://colobu.com/2016/02/29/Java-CompletableFuture/#Either
https://blog.csdn.net/qq_36597450/article/details/81232051
有话要说...