1. 介紹

如標題所見,我在使用IDEA debug ConcurrentLinkedQueueOffer方法時,發生了下面的情況。

程式碼如下:

  1. ConcurrentLinkedQueue<string> queue = new ConcurrentLinkedQueue<>();
  2. queue.offer("A");
  3. queue.offer("B");

第一種打斷點的地方:

第二種打斷點的地方:

如你所見,相同的地方,打斷點的地方不同,導致程式碼執行的路徑都不同,當時我也是迷茫的。

2. 解釋原因



IDEA 預設會開啟以上兩個配置

  • Enable alternative view for Collection classes; Select this option to display contents of classes that implement Collection, Map, or List in a more convenient format (for example, to display each map entry as a key-value pair).屬於集合的類以更加方便的格式展示其內容(例如,展示map的entry就以鍵值對展示)
  • Enable toString() object view;Allows you to configure which classes use the result of toString() as their display value.展示值以它們toString()的結果展示。

debug時,程式碼之外,額外執行的只有toString,第一個配置也會呼叫toString,所以我們定位到了罪魁禍首是toString。我們看看ConcurrentLinkedQueuetoString

2.1 toString

我們去看看哪個類中實現了toString









最後找到了AbstractCollection中實現了toString

iterator(), 在ConcurrentLinkedQueue有實現。

iterator()的實現中,跟這篇文章有關的方法是advance()

移動到下一個有效節點並返回 item 以返回 next(),如果沒有,則返回 nul

  1. private E advance() {
  2. lastRet = nextNode;
  3. E x = nextItem;
  4. Node<e> pred, p;
  5. // 第一次進來,nextNode還沒被賦值,此時預設值為null
  6. if (nextNode == null) {
  7. // 這裡就是關鍵了
  8. p = first();
  9. pred = null;
  10. } else {
  11. pred = nextNode;
  12. p = succ(nextNode);
  13. }
  14. for (;;) {
  15. if (p == null) {
  16. nextNode = null;
  17. nextItem = null;
  18. return x;
  19. }
  20. E item = p.item;
  21. if (item != null) {
  22. nextNode = p;
  23. nextItem = item;
  24. return x;
  25. } else {
  26. // skip over nulls
  27. Node<e> next = succ(p);
  28. if (pred != null && next != null)
  29. pred.casNext(p, next);
  30. p = next;
  31. }
  32. }
  33. }

返回連結串列第一個活躍的結點(非空,指向的item不為空),如果連結串列為空就返回null

  1. Node<e> first() {
  2. restartFromHead:
  3. for (;;) {
  4. for (Node<e> h = head, p = h, q;;) {
  5. boolean hasItem = (p.item != null);
  6. if (hasItem || (q = p.next) == null) {
  7. // 更新頭結點
  8. updateHead(h, p);
  9. return hasItem ? p : null;
  10. }
  11. else if (p == q)
  12. continue restartFromHead;
  13. else
  14. p = q;
  15. }
  16. }
  17. }

這篇文章也提到過,updateHead()方法。JAVA併發(4)-併發佇列ConcurrentLinkedQueue

更新頭結點,將之前的頭結點的next指向自己。

  1. final void updateHead(Node<e> h, Node<e> p) {
  2. if (h != p && casHead(h, p))
  3. h.lazySetNext(h);
  4. }

2.2 debug

我們按照最上面的程式碼且按照第一種打斷點的方式重新debug,下面會以圖片形式展示整個過程。

  • 初始狀態,此時A已進入隊成功

  • 我們知道了,使用IDEA debug時,會呼叫類的toString()方法,此時呼叫toString()方法後,狀態如下

此時,Node1next被修改成指向自身。

這裡也是網上很多部落格會認為,第一次入隊後,會把第一個節點的next指向自身的原因,其實並不會的。

  • 當我們debug到queue.offer("B")時,此時執行到offer()方法中的else if (p == q)時,就為true

3. 總結

經過了上面的分析,大家應該知道為什麼會出現文章開頭的問題了吧。也許你會迷迷糊糊的,因為涉及到了ConcurrentLinkedQueue的原始碼分析。

那我就用一句話,告訴你原因吧,當使用IDEA debug時,預設那兩個配置是啟用的,兩個配置會呼叫toString,我們應該清楚toString是否被重寫;是否影響debug某個類時,程式碼的執行路徑

可能你會覺得是IDEAbug(我當時也這樣認為),但是我們先看看下面取消兩個配置前後的debug情況

  • 取消配置前

  • 取消配置

一眼就可以看出,取消配置前debug時,更加直觀。

可能你會認為是Doug Leabug(反正我不敢這麼想。當然這句話只是開玩笑啦)。

我只是讓大家記住IDEAdebug時會存在這樣的問題,大家也可以在評論區告訴其他同學,除了ConcurrentLinkedQueue外,還有哪些類,在哪種情況下會存在這樣的問題

可能大家會有疑問,在debug時,呼叫了toString,那是否影響後續的執行。不會的,因為tail節點會被修改的在後續的執行中。可以結合上面那篇部落格,就很清楚了。

4. 參考