前段时间在 RocketMQ 的 ISSUE 里面冲浪的时候,看到一个 pr,虽说是在 RocketMQ 的地盘上发现的,但是这个玩意吧,其实和 RocketMQ 没有任何关系。
纯纯的就是 JDK 的一个 BUG。
我先问你一个问题:LinkedBlockingQueue 这个玩意是线程安全的吗?
这都是老八股文了,你要是不能脱口而出,应该是要挨板子的。
答案是:是线程安全的,因为有这两把锁的存在。
但是在 RocketMQ 的某个场景下,居然稳定复现了 LinkedBlockingQueue 线程不安全的情况。
先说结论: LinkedBlockingQueue 的 stream 遍历的方式,在多线程下是有一定问题的,可能会出现死循环。
老有意思了,这篇文章带大家盘一盘。
搞个Demo
Demo 其实都不用我搞了,前面提到的 pr 的链接是这个:
https://github.com/apache/rocketmq/pull/3509
在这个链接里面,前面围绕着 RocketMQ 讨论了很多。
但是在中间部分,一个昵称叫做 areyouok 的大佬一针见血,指出了问题的所在。
直接给出了一个非常简单的复现代码。而且完全把 RocketMQ 的东西剥离了出去:
正所谓前人栽树后人乘凉,既然让我看到了 areyouok 这位大佬的代码,那我也就直接拿来当做演示的 Demo 了。
如果你不建议的话,为了表示我的尊敬,我斗胆说一声:感谢雷总的代码。
我先把雷总的代码粘出来,方便看文章的你也实际操作一把:
public class TestQueue { public static void main(String[] args) throws Exception { LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<>(1000); for (int i = 0; i < 10; i++) { new Thread(() -> { while (true) { queue.offer(new Object()); queue.remove(); } }).start(); } while (true) { System.out.println("begin scan, i still alive"); queue.stream() .filter(o -> o == null) .findFirst() .isPresent(); Thread.sleep(100); System.out.println("finish scan, i still alive"); } } }
介绍一下上面的代码的核心逻辑。
首先是搞了 10 个线程,每个线程里面在不停的调用 offer 和 remove 方法。
需要注意的是这个 remove 方法是无参方法,意思是移除头节点。
再强调一次:LinkedBlockingQueue 里面有 ReentrantLock 锁,所以即使多个线程并发操作 offer 或者 remove 方法,也都要分别拿到锁才能操作,所以这一定是线程安全的。
然后主线程里面搞个死循环,对 queue 进行 stream 操作,看看能不能找到队列里面第一个不为空的元素。
这个 stream 操作是一个障眼法,真正的关键点在于 tryAdvance 方法:
先在这个方法这里插个眼,一会再细嗦它。
按理来说,这个方法运行起来之后,应该不停的输出这两句话才对:
begin scan, i still alive finish scan, i still alive
但是,你把代码粘出去用 JDK 8 跑一把,你会发现控制台只有这个玩意:
或者只交替输出几次就没了。
但是当我们不动代码,只是替换一下 JDK 版本,比如我刚好有个 JDK 15,替换之后再次运行,交替的效果就出来了:
那么基于上面的表现,我是不是可以大胆的猜测,这是 JDK 8 版本的 BUG 呢?
现在我们有了能在 JDK 8 运行环境下稳定复现的 Demo,接下来就是定位 BUG 的原因了。
啥原因呀?
先说一下我拿到这个问题之后,排查的思路。
非常的简单,你想一想,主线程应该一直输出但是却没有输出,那么它到底是在干什么呢?
我初步怀疑是在等待锁。
怎么去验证呢?
朋友们&