java future专题 2-6 CompletableFuture源码探秘-异常处理(1)

 1.问题引出

CompletableFuture是异步编程的很好工具,那么对于异步编程,最令人担心的现象就是异常被吞掉了。对应的,同步场景,因为天然是同步等结果,无非就是这么几种情况,返回正常结果,抛出异常,等待超时(设置了超时时间),一直等待(没有设置超时时间),但是对于异步场景来说,无论是监听还是会掉,都有可能因为使用方式的错误导致没有会掉或者没有触发监听,造成的结果就是这次的请求的结果凭空丢失了。

举个最简单的例子:

这是一个CompletableFuture最普通的使用方式,异步执行一个任务,如图所示是一个http请求,拿到结果之后执行complete方法,会触发该future的thenRun方法。但是如果这个http请求抛出了异常会怎么样呢?因为没有catch异常并对异常进行相应的处理,所以这个future永远也不会执行,相当于这个http的结果丢了,回调也不会触发了。

那么正确的做法是什么呢?

这样的话在http请求抛出异常的时候,通过completeExceptionally方法触发了future的回调。这就是一个简单的对于异常的处理,防止异常导致无回调的现象发生。但是本篇文章并不是这么简单的就结束了,而是刚刚开始。因为CompletableFuture提供了很强大的功能,封装了很多方法,这些方法如果使用不当,即使你考虑了异常,可能也不会按照你设想的方式去执行,比如:

这里用thenRun替换掉了上面的whenComplete,那么即使http请求抛出了异常,然后也正确的completeExceptionally了,也不会触发thenRun。诸如此类的情况还有很多,下面我们就一一列举说明。

2.举例分析

1.thenRun

上面已经提到了,thenRun方法是无法被异常回调触发的,我们再把图贴一下:

也就是说,即使http请求抛出了异常,并在catch中给future设置了异常,但是给future通过thenRun添加的runnable的action不会被执行。原因是什么呢?这里的completeExceptionally会触发postComplete,进而调用CompletableFuture的uniRun,至于为什么会调用postComplete,又怎么调用到uniRun,请参考之前的文章:java future专题 2-1 CompletableFuture源码探秘-基础用法(1)_普通网友的博客-CSDN博客,不在这里赘述。

我们再来看一下uniRun的源码:

这里a.result就是通过completeExceptionally设置的异常的封装,即AltResult包裹的异常,它不是null,所以会走到第二个if块中。这里r是AltResult,所以走到第一个if块中,不会走else。在这个if块中,直接调用了completeThrowable方法,这个方法只会用UnSafe设置值,不会有其他的附属动作。

也就是说按照上述写法,如果出现了异常,就会导致thenRun中的逻辑无法被执行,同时这个异常被吞掉了。

那么怎么做才能在使用uniRun的同时,在遇到异常情况,还能感知到这个异常呢?我们看一下uniRun后面的逻辑:

uniRun设置完异常值之后,返回了true,所以会走到最下面的d.postFire,所以在d上添加的响应可以被触发,那么这个d是什么呢?它就是在调用thenRun方法添加action时候创建的CompletableFuture,也是该方法的返回值,那么我们需要做的就是对这个d补充一个action:

这里最后一个whenComplete就可以触发。

究其根本,其实是UniWhenCompletion和UniRun这两种不同的UniCompletion的实现导致的,我们再看下UniWhenCompletion的源码:

这里调用了CompletableFuture的uniWhenComplete方法:

这里无论是能够获得正常结果还是抛出了异常,都会调用f.accept方法,也就保证了即使有异常,也不会出现被忽略掉的情况。

2.thenCombine

看完了简单的场景,我们再看一个相对复杂一点的场景,即thenCombine的使用,它的底层源码逻辑参考:java future专题 2-4 CompletableFuture源码探秘-高级用法(1)_普通网友的博客-CSDN博客

我们先来看demo:

有了上面thenRun对于异常处理的经验,我们可以猜到,这里的thenCombine这个action不会被执行,我们看下原因

如图源码所示,这里当future1和future2都结束之后,会走到tryComplete块中,这里面第一个if判断,r是future1 exception,它是被包装成AltResult的异常,所以在第一个if块中直接就break掉,然后到最后返回true,所以这个action的apply方法没有机会被执行。这里面的r和s分别对应a和b的结果,a是future1,b是future2,所以永远都是future1的结果被设置到了当前的CompletableFuture中(这里我们起个名字较exception future,其实它就是调用thenCombine过程中创建的d)。

那么怎么修补这个问题呢?参考上面的做法,我们可以为thenCombine也添加一个whenComplete的action

这样这个whenComplete添加的action就可以被执行了。到这里,细心地小伙伴可能会发现,这个action有两个参数,一个是result,一个是throwable,但是上面的future1和future2都抛出了异常,这个throwable是哪个异常呢?如果是其中一个,那么另一个异常呢?通过代码运行,我们可以看出这个throwable是future1 exception,我们通过源码分析下原因。

上面提到了,对于exception future,永远都是future1的结果被设置到了当前的CompletableFuture中。

紧接着触发whenComplete添加的action,这个action是exception future触发的action。还是继续看uniWhenComplete方法:

这里的r是AltResult,所以调用f.accept的时候会作为参数传入,它就是future1对应的异常。到这里回答完了第一个问题,throwable是future1对应的异常。

然后我们来回答第二个问题,第二个异常哪里去了?我们通过上面biApply方法可以看到,当把r的结果通过completeThrowable设置之后就直接break掉了,s的结果(也就是future2的结果)就被丢掉了。

那么整体看来,我们虽然正确使用了相应的方法,保证异常能够被拿到,但是当两个future都返回异常的时候,其中一个又被丢掉了,所以我们还是需要再考虑一下,做一点改变。

经过分析,我们知道combine.whenComplete的action触发的时候,两个future都已经执行完了,只不过第一个future(r)的异常结果设置完之后直接break掉了,所以这个whenComplete的参数throwable只是r的异常。那么我们可以在whenComplete方法中添加一些代码:

虽然在参数上只有一个throwable,但是在action方法体里面,我们可以通过future的get方法来拿到future1以及future2的异常结果,而且这个get不会阻塞,因为结果已经拿到了。

到此为止,thenCombine方法的双异常场景的处理流程就说完了。

其实这个场景还可以用另一种方式来取代,比如allOf,具体可以参考:java future专题 2-5 CompletableFuture源码探秘-高级用法(2)_普通网友的博客-CSDN博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值