读者诡异问题,让我发现IDEA的Bug
作者丨安琪拉
来源丨安琪拉的博客
读者的一个问题让我发现了IDEA的一个bug。
前天下午有读者在国服并发群问了个问题:
他们看完我这篇文章估计会觉得哦,好吧拉哥原来还是Google来的,原来这么简单,哈哈哈,~~~
我来大致说下这位读者所说的问题:
他写了这么一段代码:
public class ConcurrentLinkedQueueTest {
public static void main(String[] args) {
ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue();
concurrentLinkedQueue.offer(1);
}
}
然后debug 打算看 ConcurrentLinkedQueue offer函数的源码,但是发生了非常蹊跷甚至算诡异的事情。
我这里先大致交代一下 ConcurrentLinkedQueue
是做什么的,ConcurrentLinkedQueue 是并发非阻塞队列,并发编程场景里面,除了使用阻塞队列,类似BlockingQueue以外,还有非阻塞队列,ConcurrentLinkedQueue 就属于非阻塞队列。
阻塞队列使用锁来做线程控制,非阻塞队列采用CAS + 循环来做并发控制。
ConcurrentLinkedQueue 队列有head、tail 节点(头、尾),尾节点不一定是最后一个插入的节点,这里可能会有点难理解,Doug Lea在这里有些巧妙的设计。
ConcurrentLinkedQueue 实现机制不是今天的重点,后面具体讲并发编程系列的时候会讲到,今天只要知道上面交代的背景,我们继续。
第一步:new ConcurrentLinkedQueue()
会初始化头和尾节点。
可以看到初始化完成之后,head、tail都指向 $Node@524, 你可以姑且把它当做内存地址;
第二步,ConcurrentLinkedQueue.offer(1); 向队列中插入一个元素
代码如下图,插入元素都会初始化一个新Node节点来存放要插入的值,初始化的Node值指向 $Node@528;
到这里都没什么问题,我们继续往下走。
一直到 p.casNext, 大家看下面,p.casNext 是通过cas 将p 节点的next节点设置为新插入的newNode 节点,但是见证奇迹的时候到了。继续放下看。
正常casNext 之后应该如下图所示:
但是实际我们看下debug 模式下head、tail 的数据。
head 指向新节点,tail 没变,但是tail.next 指向自己。这什么情况?我当时第一反应Doug Lea 搞了什么黑科技?!!!
但是实在不敢相信,看了这么多源码还看不懂这么一小段代码,我潜意识觉得肯定有什么地方不对。于是Google 搜了一下,在全球最大同性交友网站Stack Overflow找到了问题,但是没有人回答原因。
问题:https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly
看了这个回答,赶紧试了一下,问题解决,配置就是把 IDEA的二项配置去掉就好了。
但是不爽,去IDEA 看了二项配置的解释:
Enable alternative view for Collections classes 是IDEA 会为集合创建开启一个视图。
Enable toString 会为所有对象开启toString方法,建议关闭,因为如果有大对象,debug 模式toString 非常耗性能,而且debug模式 toString 是全局的,有时候debug 模式卡的你怀疑人生。
继续说下为什么会出现 head 节点被篡改的原因,都指向新插入的newNode节点上去了。原因是debug模式开启了toString 和 集合视图,这二个属性开启都会导致IDEA 为 ConcurrentLinkedQueue 生成迭代器,遍历集合,遍历的时候调用 first() 函数,在ConcurrentLinkedQueue first() 中 head节点被更新了。
注释也很清楚,first 是将head 节点更新为第一个item值不是null 的节点,如果队列只有一个节点(除了头结点),就设置为这个头结点,上面那个bug(为什么head指向newNode节点)就都解释清楚了。
上面offer 那个 for 循环那段代码我简单解释一下,就是找到最后一个节点,然后将newNode插入到最后一个节点的后面,如果tail 节点不是最后一个节点,这时候在最后一个节点成功插入元素了,这时候CAS 更新tail为新插入的节点( casTail(t, newNode) ) 。还记得我前面说过的tail 节点不一定指向最后一个节点,所以要找最后一个节点。
ConcurrentLinkedQueue 后面讲并发编程的时候还会再详细介绍,今天的IDEA bug分析先到这里。
同时在分析ConcurrentLinkedQueue的时候,我还发现 《Java并发编程的艺术》这本书的作者方腾飞的一个错误,如下:
他说的:
只有一种可能p节点和p的next节点都为空,表示这个队列刚初始化,正准备添加第一个节点
实际上是错误的,是因为poll 移除元素的时候会出现p.next = p 自引用的情况,而不是初始化,初始化 p = null,p
.next = p 肯定直接NPE了。
最后上面这张图上还有个 “Hide null elements in arrays and collections” 这个建议取消掉。
不取消会有什么效果:
null 元素都没展示出来。有时候debug 容易忽视null 的存在,导致出现问题,虽然有一行提示:Not showing null elements。
关闭后如下图。
我是安琪拉,今天分享先到这里。
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取