看完JDK并发包源码的这个性能问题,我惊了!
时间:2021-10-11 14:12:12
[导读]国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前100混个大赛的文化衫就行了。现在还混在前50的队伍里面,稳的一比。其实我觉得大家做柔性负载均衡那题的思路其实都不会差太多,就看谁能把关键的信息收集起来并利用上了。由于是基于Dubbo去做的嘛,调试的过程中,写着...
国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前 100 混个大赛的文化衫就行了。现在还混在前 50 的队伍里面,稳的一比。

org.apache.dubbo.rpc.protocol.AbstractInvoker#waitForResultIfSync

从我直观上来说,这里用 get() 方法也应该是没有任何毛病的,甚至更好理解一点。但是,为什么没有用 get() 方法呢?其实方法上的注释已经写到原因了,就怕我这样的人产生了这样的疑问:

java.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit) 而不是 get() 方法,因为 get 方法被证明会导致性能严重的下降。对于 Dubbo 来说, waitForResultIfSync 方法,是主链路上的方法。我个人觉得保守一点说,可以说 90% 以上的请求都会走到这个方法来,阻塞等待结果。所以如果该方法如果有问题,则会影响到 Dubbo 的性能。Dubbo 作为中间件,有可能会运行在各种不同的 JDK 版本中,对于特定的 JDK 版本来说,这个优化确实是对于性能的提升有很大的帮助。就算不说 Dubbo ,我们用到 CompletableFuture 的时候,get() 方法也算是我们常常会用到的一个方法。另外,这个方法的调用链路我可太熟悉了。因为我两年前写的第一篇公众号文章就是探讨 Dubbo 的异步化改造的。当年,这部分代码肯定不是这样的,至少没有这个提示。因为如果有这个提示的话,我肯定第一次写的时候就注意到了。果然,我去翻了一下,虽然图片已经很模糊了,但是还是能隐约看到,之前确实是调用的 get() 方法:
我还称之为最“骚”的一行代码。因为这一行的代码就是 Dubbo 异步转同步的关键代码。


啥性能问题?
根据 Dubbo 注释里面的这点信息,我也不知道啥问题,但是我知道去哪里找问题。这种问题肯定在 openJDK 的 bug 列表里面记录有案,所以第一站就是来这里搜索一下关键字:https://bugs.openjdk.java.net/projects/JDK/issues/一般来说,都是一些陈年老 BUG,需要搜索半天才能找到自己想要的信息。但是,这次运气好到爆棚,弹出来的第一个就是我要找的东西,简直是搞的我都有点不习惯了,这难道是传说中的国庆献礼吗,不敢想不敢想。

https://bugs.openjdk.java.net/browse/JDK-8227019我们一起看看这个 BUG 描述的是啥玩意。

spins = (Runtime.getRuntime().availableProcessors() > 1) ?
1 << 8 : 0; // Use brief spin-wait on multiprocessors
他说位于 waitingGet 里面,我们就去看看到底是怎么回事嘛。但是我本地的 JDK 的版本是 1.8.0_271,其 waitingGet 源码是这样的:java.util.concurrent.CompletableFuture#waitingGet

spins=SPINS ,虽然 SPINS 调用了 Runtime.getRuntime().availableProcessors() 方法,但是该字段被 static 和 final 修饰了,也就不存在 BUG 中描述的“频繁调用”了。于是我意识到我的版本是不对的,这应该是被修复之后的代码,所以去下载了几个之前的版本。最终在 JDK 1.8.0_202 版本中找到了这样的代码:
Runtime.getRuntime().availableProcessors() 方法的返回缓存了起来。我一定要找到这行代码的原因就是要证明这样的代码确实是在某些 JDK 版本中出现过。好了,现在我们看一下 waitingGet 方法是干啥的。首先,调用 get() 方法的时候,如果 result 还是 null 那么说明异步线程执行的结果还没就绪,则调用 waitingGet 方法:






http://cr.openjdk.java.net/~shade/8227018/webrev.01/src/share/classes/java/util/concurrent/CompletableFuture.java.udiff.html



你别慌啊,猴急猴急的,我这不是还没说完嘛?我们再把目光放到图片中的这句话上:

http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/f3af17da360b

依据就在这个 BUG 链接里面提到的编号为 8227018 的 BUG 中,它们其实描述的是同一个事情:

《Java 并发编程实战》的作者之一,端茶就完事了。

到底啥原因?
前面噼里啪啦的说了这么大一段,核心思想其实就是 Runtime.availableProcessors 方法的调用成本高,所以在 CompletableFuture.waitingGet 方法中不应该频繁调用这个方法。但是 availableProcessors 为什么调用成本就高了,依据是啥,得拿出来看看啊!这一小节,就给大家看看依据是什么。依据就在这个 BUG 描述中:https://bugs.openjdk.java.net/browse/JDK-8157522


public static void main(String[] args) throws Exception {
AtomicBoolean stop = new AtomicBoolean();
AtomicInteger count = new AtomicInteger();
new Thread(() -> {
while (!stop.get()) {
Runtime.getRuntime().availableProcessors();
count.incrementAndGet();
}
}).start();
try {
int lastCount = 0;
while (true) {
Thread.sleep(1000);
int thisCount = count.get();
System.out.printf("%s calls/sec%n", thisCount - lastCount);
lastCount = thisCount;
}
}
finally {
stop.set(true);
}
}
按照 BUG 提交者的描述,如果你在 64 位的 Linux 上,分别用 JDK 1.8b182 和 1.8b191 版本去跑,你会发现有近 100 倍的差异。至于为什么有 100 倍的性能差异,一位叫做 Fairoz Matte 的老哥说他调试了一下,定位到问题出现在调用 “OSContainer::is_containerized()” 方法的时候:

再看get方法
现在我们知道了这个没有卵用的知识点之后,我们再看看为什么调用带超时时间的 get() 方法,没有这个问题。java.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit)首先可以看到内部调用的方法都不一样了:



asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS) 如果我们改成 asyncResult.get() 效果还是一样的吗?肯定是不一样的。再说一次:Dubbo 作为开源的中间件,有可能会运行在各种不同的 JDK 版本中,且该方法是它主链路上的核心代码,对于特定的 JDK 版本来说,这个优化确实是对于性能的提升有很大的帮助。所以写中间件还是有点意思哈。最后,再送你一个为 Dubbo 提交源码的机会。在其下面的这个类中:org.apache.dubbo.rpc.AsyncRpcResult还是存在这两个方法:








