再说说 get 方法
最后,再看看 get 方法吧。之前发布的《看完JDK并发包源码的这个性能问题,我惊了!》这篇文章,有朋友看了之后有几个问题,我再串起来讲一下。
CompletableFuture 提交任务的方式有两种:
一种是 supplyAsync 带返回值的。
一种是 runAsync 返回值是 void 的,相当于没有返回值。
比如,我们用 supplyAsync 的时候:
就刻意返回一个 null。
我还可以扩展一下,假设我们的方法用的是 runAsync,本来就没有返回值的。
比如这样:
你看这里的判断条件是 (r = result) == null
。
那么问题就来了,假设这个方法的返回值本来就是 null,也就是我们上面的情况,怎么办呢?
为 null 就有三种情况了:
- 1.是 runAsync 这种,真的没有返回值,所以就算任务执行完成了,get 出来的确实就是 null。
- 2.是有返回值的,只是目前任务还没执行完成,所以 result 还是 null。
- 3.是有返回值的,返回的值就是 null。
怎么去分别出这三种情况呢?
那么就要看看这个 result 赋值的地方了,用脚指头猜也知道在这里搞了一些事情。
所以简单的找寻一番之后,可以找到这个关键的地方:
框起来的代码,目的是为了获取 CompletableFuture 类中的 result 字段的偏移量,并用大写的 RESULT 存储起来。
有经验的朋友看到这里大概就知道它要用 compareAndSwapObject 这个骚操作了:
答案就是我框起来的部分:在 CompletableFuture 里面,把 null 也封装到 AltResult 对象里面了。
基于此,可以区分出前面我说的那三种情况。
你看这里有一个专门的 completeNull 方法,其中的调用者就有 AysncRun 方法:
再去看看调用栈,调试一下,你就知道 runAsync 这种,真的没有返回值的是怎么处理的了。
核心技术就是把 null 封装到 AltResult 对象里面。
然后如何分別返回值就是 null 的情况呢?
都有一个代表 null 的对象了,那还不简单吗,一个小小的判断就搞定了:
最后,再提一下这个方法:
java.util.concurrent.CompletableFuture#waitingGet
我之前那篇文章里面写了这样一句话:
加入这个自旋,是为了稍晚一点执行后续逻辑中的 park 代码,这个稍重一点的操作。但是我觉得这个 “brief spin-wait” 的收益其实是微乎其微的。
有小伙伴问我 park 的逻辑在哪?
其实就在 waitingGet 的 while 循环的最后一个分支里面,也就是我框起来的部分:
最后你顺着往下 Debug ,就能找到这个地方:
java.util.concurrent.CompletableFuture.Signaller#block
这里不就是 park 的逻辑吗:
打上断点自己玩去吧。
其实还有一种骚操作,我一般不告诉别人,也简单的分享一下吧。
还是拿前面的代码做演示,这个代码你跑起来之后,主线程由于调用了 get 方法,那么势必会阻塞等待异步任务的结果:
主线程是 park 起来的,在哪被 park 起来的呢?
at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1707)
这不就是我刚刚给你说的方法吗?
然后你在这里打上断点,看一下调用堆栈,不就把主链路玩得明明白白的嘛:
怎么样,这波逆向操作,溜不溜,分分钟就学会了。
找到了 park 的地方,那么在哪儿被 unpark 的呢?
这还不简单吗?
反正我一搜就搜出来了:
然后再在 unpark 这里打上一个断点:
唤醒流程也可以调试的明明白白。
好了,挂起和唤醒都给你定位到关键地方了,就到这,玩去吧。
本文已收录自个人博客,欢迎大家来玩。