读者诡异问题,让我发现IDEA的Bug

源码共读

共 3737字,需浏览 8分钟

 ·

2021-11-09 12:31

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇


作者丨安琪拉

来源丨安琪拉的博客


读者的一个问题让我发现了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 节点,但是见证奇迹的时候到了。继续放下看。

image-20211103222109757

正常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” 这个建议取消掉。

不取消会有什么效果:

image-20211103232458655

null 元素都没展示出来。有时候debug 容易忽视null 的存在,导致出现问题,虽然有一行提示:Not showing null elements。

关闭后如下图。


我是安琪拉,今天分享先到这里。

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报