我惊了!CompletableFuture居然有性能问题! (上)

简介: 我惊了!CompletableFuture居然有性能问题! (上)

你好呀,我是歪歪。

国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前 100 混个大赛的文化衫就行了。

现在还混在前 50 的队伍里面,稳的一比。

image.png

其实我觉得大家做柔性负载均衡那题的思路其实都不会差太多,就看谁能把关键的信息收集起来并利用上了。

由于是基于 Dubbo 去做的嘛,调试的过程中,写着写着我看到了这个地方:

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

image.png

先看我框起来的这一行代码,aysncResult 的里面有有个 CompletableFuture ,它调用的是带超时时间的 get() 方法,超时时间是 Integer.MAX_VALUE,理论上来说效果也就等同于 get() 方法了。

从我直观上来说,这里用 get() 方法也应该是没有任何毛病的,甚至更好理解一点。

但是,为什么没有用 get() 方法呢?

其实方法上的注释已经写到原因了,就怕我这样的人产生了这样的疑问:

image.png

抓住我眼球的是这这几个单词:

have serious performance drop。

性能严重下降。

大概就是说我们必须要调用 java.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit) 而不是 get() 方法,因为 get 方法被证明会导致性能严重的下降。

对于 Dubbo 来说, waitForResultIfSync 方法,是主链路上的方法。我个人觉得保守一点说,可以说 90% 以上的请求都会走到这个方法来,阻塞等待结果。所以如果该方法如果有问题,则会影响到 Dubbo 的性能。

Dubbo 作为中间件,有可能会运行在各种不同的 JDK 版本中,对于特定的 JDK 版本来说,这个优化确实是对于性能的提升有很大的帮助。

就算不说 Dubbo ,我们用到 CompletableFuture 的时候,get() 方法也算是我们常常会用到的一个方法。

另外,这个方法的调用链路我可太熟悉了。

因为我两年前写的第一篇公众号文章就是探讨 Dubbo 的异步化改造的,《Dubbo 2.7新特性之异步化改造》

当年,这部分代码肯定不是这样的,至少没有这个提示。

因为如果有这个提示的话,我肯定第一次写的时候就注意到了。

果然,我去翻了一下,虽然图片已经很模糊了,但是还是能隐约看到,之前确实是调用的 get() 方法:

image.png

我还称之为最“骚”的一行代码。

因为这一行的代码就是 Dubbo 异步转同步的关键代码。

image.png

前面只是一个引子,本文不会去写 Dubbo 相关的知识点。

主要写写 CompletableFuture 的 get() 到底有啥问题。

放心,这个点面试肯定不考。只是你知道这个点后,恰好你的 JDK 版本是没有修复之前的,写代码的时候可以稍微注意一下。

学 Dubbo 在方法调用的地方加上一样的 NOTICE,直接把逼格拉满。等着别人问起来的时候,你再娓娓道来。

或者不经意间看到别人这样写的时候,轻飘飘的说一句:这里有可能会有性能问题,可以去了解一下。

image.png


啥性能问题?


根据 Dubbo 注释里面的这点信息,我也不知道啥问题,但是我知道去哪里找问题。

这种问题肯定在 openJDK 的 bug 列表里面记录有案,所以第一站就是来这里搜索一下关键字:

https://bugs.openjdk.java.net/projects/JDK/issues/

一般来说,都是一些陈年老 BUG,需要搜索半天才能找到自己想要的信息。

但是,这次运气好到爆棚,弹出来的第一个就是我要找的东西,简直是搞的我都有点不习惯了,这难道是传说中的国庆献礼吗,不敢想不敢想。

image.png

标题就是:对CompletableFuture的性能改进。

里面提到了编号为 8227019 的 BUG。

https://bugs.openjdk.java.net/browse/JDK-8227019

我们一起看看这个 BUG 描述的是啥玩意。

image.png

标题翻译过来,大概意思就是说 CompletableFuture.waitingGet 方法里面有一个循环,这个循环里面调用了 Runtime.availableProcessors 方法。且这个方法被调用的很频繁,这样不好。

在详细描述里面,它提到了另外的一个编号为 8227006 的 BUG,这个 BUG 描述的就是为什么频繁调用 availableProcessors 不太好,但是这个我们先按下不表。

先研究一下他提到的这样一行代码:

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

image.png

先不管这几行代码是啥意思吧,反正我发现没有看到 bug 中提到的代码,只看到了 spins=SPINS ,虽然 SPINS 调用了 Runtime.getRuntime().availableProcessors() 方法,但是该字段被 static 和 final 修饰了,也就不存在 BUG 中描述的“频繁调用”了。

于是我意识到我的版本是不对的,这应该是被修复之后的代码,所以去下载了几个之前的版本。

最终在 JDK 1.8.0_202 版本中找到了这样的代码:


image.png


和前面截图的源码的差异就在于前者多了一个 SPINS 字段,把 Runtime.getRuntime().availableProcessors() 方法的返回缓存了起来。

我一定要找到这行代码的原因就是要证明这样的代码确实是在某些 JDK 版本中出现过。

好了,现在我们看一下 waitingGet 方法是干啥的。

首先,调用 get() 方法的时候,如果 result 还是 null 那么说明异步线程执行的结果还没就绪,则调用 waitingGet 方法:

image.png


首先把 spins 的值初始化为 -1。

然后当 result 为 null 的时候,就一直进行 while 循环。

所以,如果进入循环,第一次一定会调用 availableProcessors 方法。然后发现是多处理器的运行环境,则把 spins 置为 1<<8 ,即 256。

然后再次进行循环,走入到 spins>0 的分支判断,接着做一个随机运算,随机出来的值如果大于等于 0 ,则对 spins 进行减一操作。

只有减到 spins 为 0 的时候才会进入到后面的这些被我框起来的逻辑中:

image.png

也就是说这里就是把 spins 从 256 减到 0,且由于随机函数的存在,循环次数一定是大于 256 次的。

但是还有一个大前提,那就是每次循环的时候都会去判断循环条件是否还成立。即判断 result 是否还是 null。为 null 才会继续往下减。

所以,你说这段代码是在干什么事儿?

其实注释上已经写的很清楚了:

image.png

Use brief spin-wait on multiprocessors。

brief,这是一个四级词汇哈,得记住,要考的。就是“短暂”的意思,是一个不规则动词,其最高级是 briefest。

对了,spin 这个单词大家应该认识吧,前面忘记给大家教单词了,就一起讲了,看小黑板:

image.png

所以注释上说的就是:如果是多处理器,则使用短暂的自旋等待一下。

从 256 减到 0 的过程,就是这个“brief spin-wait”。

但是仔细一想,在自旋等待的这个过程中,availableProcessors 方法只是在第一次进入循环的时候调用了一次。

那为什么说它耗费性能呢?

是的,确实是调用 get() 方法的只调用了一次,但是你架不住 get() 方法被调用的地方多啊。

就拿 Dubbo 举例,绝大部分情况下的大家的调用方式都用的是默认的同步调用的方案。所以每一次调用都会到异步转同步这里阻塞等待结果,也就说每次都会调用一次 get() 方法,即 availableProcessors 方法就会被调用一次。

那么解决方案是什么呢?

在前面我已经给大家看了,就是把 availableProcessors 方法的返回值找个字段给缓存起来:

image.png

目录
相关文章
|
1月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
63 1
|
5月前
|
监控 安全 Java
CompletableFuture探秘:解锁Java并发编程的新境界
CompletableFuture探秘:解锁Java并发编程的新境界
209 0
|
消息中间件 存储 Java
一网打尽异步神器CompletableFuture
最近一直畅游在RocketMQ的源码中,发现在RocketMQ中很多地方都使用到了CompletableFuture,所以今天就跟大家来聊一聊JDK1.8提供的异步神器CompletableFuture,并且最后会结合RocketMQ源码分析一下CompletableFuture的使用。
|
Java
异步利刃CompletableFuture
异步利刃CompletableFuture
96 0
|
JSON 缓存 JavaScript
提高系统吞吐量的一把利器:DeferredResult 到底有多强?
提高系统吞吐量的一把利器:DeferredResult 到底有多强?
CountDownLatch翻车后,大家都建议我用CompletableFuture改造下,改造完感觉真香啊!
前段时间使用了CountDownLatch来做并发流程的控制,在生产上碰到了一些问题,最终问题是解决了,但是那篇文章的评论大家让我用CompletableFuture来试一试,改造完之后,发现CompletableFuture这东西真强大,有种相见恨晚的感觉。
我惊了!CompletableFuture居然有性能问题! (下)
我惊了!CompletableFuture居然有性能问题! (下)
558 0
我惊了!CompletableFuture居然有性能问题! (下)
|
缓存 Java
我惊了!CompletableFuture居然有性能问题! (中)
我惊了!CompletableFuture居然有性能问题! (中)
178 0
我惊了!CompletableFuture居然有性能问题! (中)
|
缓存 前端开发 Java
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(上)
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(上)
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(上)
|
Java API
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(下)
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(下)
搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文(下)