到底啥原因?
前面噼里啪啦的说了这么大一段,核心思想其实就是 Runtime.availableProcessors 方法的调用成本高,所以在 CompletableFuture.waitingGet 方法中不应该频繁调用这个方法。
但是 availableProcessors 为什么调用成本就高了,依据是啥,得拿出来看看啊!
这一小节,就给大家看看依据是什么。
依据就在这个 BUG 描述中:
标题上说:在 linux 环境下,Runtime.availableProcessors 执行时间增加了 100 倍。
增加了 100 倍,肯定是有两个不同的版本的对比,那么是哪两个版本呢?
在 1.8b191 之前的 JDK 版本上,下面的示例程序可以实现每秒 400 多万次对 Runtime.availableProcessors 的调用。
但在 JDK build 1.8b191 和所有后来的主要和次要版本(包括11)上,它能实现的最大调用量是每秒4万次左右,性能下降了100倍。
这就导致了 CompletableFuture.waitingGet 的性能问题,它在一个循环中调用了 Runtime.availableProcessors。因为我们的应用程序在异步代码中表现出明显的性能问题,waitingGet 就是我们最初发现问题的地方。
测试代码是这样的:
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()” 方法的时候:
而且他也定位到了问题出现的最开始的版本号是 8u191 b02,在这个版本之后的代码都会有这样的问题。
带来问题的那次版本升级干的事是改进 docker 容器检测和资源配置的使用。
所以,如果你的 JDK 8 是 8u191 b02 之前的版本,且系统调用并发非常高,那么恭喜你,有机会踩到这个坑。
然后,下面几位大佬基于这个问题给出了很多解决方案,并针对各种解决方案进行讨论。
有的解决方案,听起来就感觉很麻烦,需要编写很多的代码。
最终,大道至简,还是选择了实现起来比较简单的 cache 方案,虽然这个方案也有一点瑕疵,但是出现的概率非常低且是可以接受的。
再看get方法
现在我们知道了这个没有卵用的知识点之后,我们再看看为什么调用带超时时间的 get() 方法,没有这个问题。
java.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit)
首先可以看到内部调用的方法都不一样了:
有超时时间的 get() 方法,内部调用的是 timedGet 方法,入参就是超时时间。
点进 timedGet 方法就知道为什么调用带超时时间的 get() 方法没有问题了:
在代码的注释里面已经把答案给你写好了:我们故意不在这里旋转(像waitingGet那样),因为上面对 nanoTime() 的调用很像一个旋转。
可以看到在该方法内部,根本就没有对 Runtime.availableProcessors 的调用,所以也就不存在对应的问题。
现在,我们回到最开始的地方:
那么你说,下面的 asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS)
如果我们改成 asyncResult.get()
效果还是一样的吗?
肯定是不一样的。
再说一次:Dubbo 作为开源的中间件,有可能会运行在各种不同的 JDK 版本中,且该方法是它主链路上的核心代码,对于特定的 JDK 版本来说,这个优化确实是对于性能的提升有很大的帮助。
所以写中间件还是有点意思哈。
最后,再送你一个为 Dubbo 提交源码的机会。
在其下面的这个类中:
org.apache.dubbo.rpc.AsyncRpcResult
还是存在这两个方法:
完全可以把它们全部改掉调用 get(long timeout, TimeUnit unit) 方法,然后把 get() 方法直接删除了。
我觉得肯定是能被 merge 的。
如果你想为开源项目做贡献,熟悉一下流程,那么这是一个不错的小机会。