背景
最近,在復習JUC的時候調試了一把ConcurrentLinkedQueue的offer方法,意外的發現Idea在debug模式下竟然會 “自動修改” 已經創建的Java對象,當時覺得這個現象很是奇怪,現在把問題的原因以及解決過程記錄下來,希望你在調試的時候不要踩坑。
調試代碼
調試的代碼很簡單,就是多次調用offer方法,然后觀察ConcurrentLinkedQueue的head和tail屬性。
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueTest {
public static void main(String[] args) {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
print(queue);
queue.offer("aaa");
print(queue);
queue.offer("bbb");
print(queue);
queue.offer("ccc");
print(queue);
}
/**
* 打印並發隊列head屬性的identityHashCode
* @param queue
*/
private static void print(ConcurrentLinkedQueue queue) {
Field field = null;
boolean isAccessible = false;
try {
field = ConcurrentLinkedQueue.class.getDeclaredField("head");
isAccessible = field.isAccessible();
if (!isAccessible) {
field.setAccessible(true);
}
System.out.println("head: " + System.identityHashCode(field.get(queue)));
} catch (Exception e) {
e.printStackTrace();
} finally {
field.setAccessible(isAccessible);
}
}
}
調試過程
上述代碼在Idea中debug模式下head屬性會無緣無故的被修改(run模式下正常,debug模式下關閉所有斷點也正常),檢查ConcurrentLinkedQueue的源碼發現,head屬性只有在構造器和反序列化的readObject共3處地方才會被直接賦值(不是cas修改),我也仔細檢查了offer方法,確實沒有修改head的地方。而Idea在debug時,把head屬性修改為第一次offer的Node節點,這個現象就很奇怪了。
-
在run模式下的輸出結果,多次調用offer方法,head屬性都是同一個對象(debug模式下關閉所有斷點也是同樣的效果)

-
在offer方法中斷點,然后debug並單步調試(Step over)

-
在offer方法中斷點,然后debug並直接運行到下一個斷點(Resume program)

由上可見,在debug進入offer方法之后head屬性確實被修改了(對象已經不是同一個),而且這不是偶爾出現,而是一直可以復現的,Step over和Resume program也表現出了修改head屬性不同的時機,這讓人很費解。 更費解的是就算不在offer方法體里斷點,在main方法中斷點也會出現head被修改的現象。
- 轉戰到Eclipse,同樣的環境,同樣的操作,在run和debug模式下都不會出現head被修改的情況

分析
了解到Idea在debug模式下默認開啟了toString預覽特性(Settings>>Build,Execution,Deployment>>Debugger>>Data Views>>Java>>Enable 'toString()' object view),可是調用toString方法也不至於把對象本身都修改了啊,也專門看了下ConcurrentLinkedQueue的內部類Node,並沒有復寫toString方法(事后回顧,當時在這里疏忽了,下文會再介紹),但還是關掉特性再測試一遍,然而還是同樣的結果,head屬性任然被悄悄的修改了。第二天來到公司在同事的環境(IntelliJ IDEA 2019.1)上驗證了下,還是同樣的問題,排除Idea版本的因素。
郁悶了一會兒,就向"網友"提問了鏈接,不久就得到了IntelliJ IDEA的產品經理yole的回復,他的意思還是Idea 的 Data Views 的 toString 在作怪,上文已經說過關掉toString特性還是有這個問題,但是他給了我一個重要的思路就是:在debug模式下,ConcurrentLinkedQueue的對象也會被調用toString方法的,在隊列的toString方法中會獲取隊列的迭代器,而創建迭代器時會調用first方法,first方法里就會cas修改head屬性。(之前確實沒考慮到隊列本身的toString方法,而是去看Node是否重寫了toString,手動哭臉😭)
這里需要注意的是,盡管關掉toString特性上面問題還是存在,原因就在於ConcurrentLinkedQueue是一個Collection,Data Views 還有一個選項“Enable alternative view for Collections classes” (平時沒注意...)所以也會在debug時造成隊列迭代器的遍歷。把這個特性也一並關掉,則上面的問題就不會再出現了。

總結
之前看到有網友在調試低版本的fastjson的反序列化時也遇到過Idea toString的問題,雖然這可能不會影響程序的正常執行,但是作為開發人員,在debug時完全可能會遇到這種被Idea挖坑的情況。對於Data views特性我目前也持保留態度,如果不是特別依賴就直接關掉吧。
參考:
https://blog.csdn.net/dajiangqingzhou/article/details/78676459
https://www.cnblogs.com/oldtrafford/p/8612089.html
