讀者詭異問(wèn)題,讓我發(fā)現(xiàn)IDEA的Bug
作者丨安琪拉
來(lái)源丨安琪拉的博客
讀者的一個(gè)問(wèn)題讓我發(fā)現(xiàn)了IDEA的一個(gè)bug。
前天下午有讀者在國(guó)服并發(fā)群?jiǎn)柫藗€(gè)問(wèn)題:
他們看完我這篇文章估計(jì)會(huì)覺(jué)得哦,好吧拉哥原來(lái)還是Google來(lái)的,原來(lái)這么簡(jiǎn)單,哈哈哈,~~~
我來(lái)大致說(shuō)下這位讀者所說(shuō)的問(wèn)題:
他寫(xiě)了這么一段代碼:
public class ConcurrentLinkedQueueTest {
public static void main(String[] args) {
ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue();
concurrentLinkedQueue.offer(1);
}
}
然后debug 打算看 ConcurrentLinkedQueue offer函數(shù)的源碼,但是發(fā)生了非常蹊蹺甚至算詭異的事情。
我這里先大致交代一下 ConcurrentLinkedQueue 是做什么的,ConcurrentLinkedQueue 是并發(fā)非阻塞隊(duì)列,并發(fā)編程場(chǎng)景里面,除了使用阻塞隊(duì)列,類(lèi)似BlockingQueue以外,還有非阻塞隊(duì)列,ConcurrentLinkedQueue 就屬于非阻塞隊(duì)列。
阻塞隊(duì)列使用鎖來(lái)做線程控制,非阻塞隊(duì)列采用CAS + 循環(huán)來(lái)做并發(fā)控制。
ConcurrentLinkedQueue 隊(duì)列有head、tail 節(jié)點(diǎn)(頭、尾),尾節(jié)點(diǎn)不一定是最后一個(gè)插入的節(jié)點(diǎn),這里可能會(huì)有點(diǎn)難理解,Doug Lea在這里有些巧妙的設(shè)計(jì)。
ConcurrentLinkedQueue 實(shí)現(xiàn)機(jī)制不是今天的重點(diǎn),后面具體講并發(fā)編程系列的時(shí)候會(huì)講到,今天只要知道上面交代的背景,我們繼續(xù)。
第一步:new ConcurrentLinkedQueue() 會(huì)初始化頭和尾節(jié)點(diǎn)。
可以看到初始化完成之后,head、tail都指向 $Node@524, 你可以姑且把它當(dāng)做內(nèi)存地址;
第二步,ConcurrentLinkedQueue.offer(1); 向隊(duì)列中插入一個(gè)元素
代碼如下圖,插入元素都會(huì)初始化一個(gè)新Node節(jié)點(diǎn)來(lái)存放要插入的值,初始化的Node值指向 $Node@528;
到這里都沒(méi)什么問(wèn)題,我們繼續(xù)往下走。
一直到 p.casNext, 大家看下面,p.casNext 是通過(guò)cas 將p 節(jié)點(diǎn)的next節(jié)點(diǎn)設(shè)置為新插入的newNode 節(jié)點(diǎn),但是見(jiàn)證奇跡的時(shí)候到了。繼續(xù)放下看。
正常casNext 之后應(yīng)該如下圖所示:
但是實(shí)際我們看下debug 模式下head、tail 的數(shù)據(jù)。
head 指向新節(jié)點(diǎn),tail 沒(méi)變,但是tail.next 指向自己。這什么情況?我當(dāng)時(shí)第一反應(yīng)Doug Lea 搞了什么黑科技?!!!
但是實(shí)在不敢相信,看了這么多源碼還看不懂這么一小段代碼,我潛意識(shí)覺(jué)得肯定有什么地方不對(duì)。于是Google 搜了一下,在全球最大同性交友網(wǎng)站Stack Overflow找到了問(wèn)題,但是沒(méi)有人回答原因。
問(wèn)題:https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly
看了這個(gè)回答,趕緊試了一下,問(wèn)題解決,配置就是把 IDEA的二項(xiàng)配置去掉就好了。
但是不爽,去IDEA 看了二項(xiàng)配置的解釋?zhuān)?/p>
Enable alternative view for Collections classes 是IDEA 會(huì)為集合創(chuàng)建開(kāi)啟一個(gè)視圖。
Enable toString 會(huì)為所有對(duì)象開(kāi)啟toString方法,建議關(guān)閉,因?yàn)槿绻写髮?duì)象,debug 模式toString 非常耗性能,而且debug模式 toString 是全局的,有時(shí)候debug 模式卡的你懷疑人生。
繼續(xù)說(shuō)下為什么會(huì)出現(xiàn) head 節(jié)點(diǎn)被篡改的原因,都指向新插入的newNode節(jié)點(diǎn)上去了。原因是debug模式開(kāi)啟了toString 和 集合視圖,這二個(gè)屬性開(kāi)啟都會(huì)導(dǎo)致IDEA 為 ConcurrentLinkedQueue 生成迭代器,遍歷集合,遍歷的時(shí)候調(diào)用 first() 函數(shù),在ConcurrentLinkedQueue first() 中 head節(jié)點(diǎn)被更新了。

注釋也很清楚,first 是將head 節(jié)點(diǎn)更新為第一個(gè)item值不是null 的節(jié)點(diǎn),如果隊(duì)列只有一個(gè)節(jié)點(diǎn)(除了頭結(jié)點(diǎn)),就設(shè)置為這個(gè)頭結(jié)點(diǎn),上面那個(gè)bug(為什么head指向newNode節(jié)點(diǎn))就都解釋清楚了。
上面offer 那個(gè) for 循環(huán)那段代碼我簡(jiǎn)單解釋一下,就是找到最后一個(gè)節(jié)點(diǎn),然后將newNode插入到最后一個(gè)節(jié)點(diǎn)的后面,如果tail 節(jié)點(diǎn)不是最后一個(gè)節(jié)點(diǎn),這時(shí)候在最后一個(gè)節(jié)點(diǎn)成功插入元素了,這時(shí)候CAS 更新tail為新插入的節(jié)點(diǎn)( casTail(t, newNode) ) 。還記得我前面說(shuō)過(guò)的tail 節(jié)點(diǎn)不一定指向最后一個(gè)節(jié)點(diǎn),所以要找最后一個(gè)節(jié)點(diǎn)。
ConcurrentLinkedQueue 后面講并發(fā)編程的時(shí)候還會(huì)再詳細(xì)介紹,今天的IDEA bug分析先到這里。
同時(shí)在分析ConcurrentLinkedQueue的時(shí)候,我還發(fā)現(xiàn) 《Java并發(fā)編程的藝術(shù)》這本書(shū)的作者方騰飛的一個(gè)錯(cuò)誤,如下:
他說(shuō)的:
只有一種可能p節(jié)點(diǎn)和p的next節(jié)點(diǎn)都為空,表示這個(gè)隊(duì)列剛初始化,正準(zhǔn)備添加第一個(gè)節(jié)點(diǎn)
實(shí)際上是錯(cuò)誤的,是因?yàn)閜oll 移除元素的時(shí)候會(huì)出現(xiàn)p.next = p 自引用的情況,而不是初始化,初始化 p = null,p
.next = p 肯定直接NPE了。
最后上面這張圖上還有個(gè) “Hide null elements in arrays and collections” 這個(gè)建議取消掉。
不取消會(huì)有什么效果:
null 元素都沒(méi)展示出來(lái)。有時(shí)候debug 容易忽視null 的存在,導(dǎo)致出現(xiàn)問(wèn)題,雖然有一行提示:Not showing null elements。
關(guān)閉后如下圖。
我是安琪拉,今天分享先到這里。
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤(pán)了,歡迎下載!

面試題】即可獲取
