併發程式設計之 ConcurrentLinkedQueue 原始碼剖析

莫那·魯道發表於2018-04-30

前言

今天我們繼續分析 java 併發包的原始碼,今天的主角是誰呢?ConcurrentLinkedQueue,上次我們分析了併發下 ArrayList 的替代 CopyOnWriteArrayList,這次分析則是併發下 LinkedArrayList 的替代 ConcurrentLinkedQueue, 也就是併發連結串列。

Demo

Demo

該類繼承結構如下:

繼承圖

該類是 Collection 框架下的實現。也就是Java 類庫提供的資料結構。

add 方法將指定元素插入此佇列的尾部。 poll 方法 獲取並移除此佇列的頭,如果此佇列為空,則返回 null。 peek 方法 獲取但不移除此佇列的頭;如果此佇列為空,則返回 null。

那麼我們就看看 doug lea 是如何實現併發安全的吧。在這之前,我們可以試想一下,實現併發安全無非兩種方式,一種是鎖,就像我們之前分析的容器,比如 concurrentHashMap,CopyOnWriteArrayList , LinkedBolckingQueue,還有一種是 CAS,在這些容器裡也用到了。那麼,如果是我們來實現這個佇列,使用什麼方式呢?有趣的問題。

開始看原始碼吧。

add 方法原始碼剖析

實際上是呼叫 offer 方法,add 方法是 Collection 介面規定的容器方法,而 offer 方法是 Queue 介面的方法。

add方法

那我們就看看 offer 方法:

    public boolean offer(E e) {
        // 檢查是否是null,如果是null ,丟擲NullPointerException
        checkNotNull(e);
        // 建立一個node 物件,使用  CAS 建立物件
        final Node<E> newNode = new Node<E>(e);
        // 輪詢連結串列節點,知道找到節點的 next 為null,才會進行賦值
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // 找到null值之後將剛剛建立的值通過CAS放入
                if (p.casNext(null, newNode)) {
                    // 因為 p 遍歷在輪詢後會變化,因此需要判斷,如果不相等,則使用CAS將新節點作為尾部節點。
                    if (p != t)
                        casTail(t, newNode);  // Failure is OK.
                     // 放入成功後返回 ture
                    return true;
                }
            }
            // 輪詢後  p 有可能等於 q,此時,就需要對 p 重新賦值。
            else if (p == q)
                // 這裡需要注意一下:判斷t != t,是因為併發下可能 tail 被改了,如果被改了,則使用新的 t,否則從連結串列頭重新輪詢。
                p = (t != (t = tail)) ? t : head;
            else
                // 同樣,當 t 不等於 p 時,說明 p 在上面被重新賦值了,並且 tail 也被別的執行緒改了,則使用新的 tail,否則迴圈檢查p的下個節點
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }
複製程式碼

程式碼行數很少,樓主註釋也寫了,這裡可以看到 doug lea 使用了 CAS 的方式防止併發錯誤,同時,也看得出對 tail 變數被修改的擔憂,通過 t != t 的判斷,來檢查 tail 是否被其他執行緒修改了,而這個offer 操作,如果不成功,則永遠不會返回,這個佇列同時也是無界的。這點在使用的時候需要注意一下。

那麼 poll 方法如何實現呢?

poll 方法原始碼剖析

    public E poll() {
        // 迴圈跳出標記,類似goto
        restartFromHead:
        // 死迴圈
        for (;;) {
            // 死迴圈,從 head 開始遍歷
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                // 如果 head 不是null 且 將 head 的 item 屬性設定為null成功,則返回並更新頭節點
                if (item != null && p.casItem(item, null)) {
                    // 如果 p != h 說明在 p 輪詢時被修改了
                    if (p != h) 
                         // 如果p 的next 屬性不是null ,將 p 作為頭節點,而 q 將會消失
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                // 如果 p(head) 的 next 節點 q 也是null,則表示沒有資料了,返回null,則將 head 設定為null
                // 注意:  updateHead 方法最後還會將原有的 head 作為自己 next 節點,方便offer 連線。
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                // 如果 p == q,說明別的執行緒取出了 head,並將 head 更新了。就需要重新開始
                else if (p == q)
                    // 從頭開始重新迴圈
                    continue restartFromHead;
               // 如果都不是,則將 h 的 next 賦給 h,並重新迴圈。
                else
                    p = q;
            }
        }
    }
複製程式碼

上面樓主已經寫了註釋,但是有一個非常困擾哦樓主的疑點,就是 else if (p == q) 這行程式碼,樓主分析的沒有問題,但是再樓主的單執行緒測試這段程式碼時,出現了詭異的情況,無法解釋,因此, 樓主貼出測試用例,大家一起看看:

測試程式碼:

併發程式設計之 ConcurrentLinkedQueue 原始碼剖析

斷點程式碼:

併發程式設計之 ConcurrentLinkedQueue 原始碼剖析

注意,斷點位置一定要和我的一致。會出現一些奇怪的效果。樓主無法解釋,因為這個問題,樓主一直都不敢發這篇文章出來,但樓主覺得有必要說出這個問題,拋磚引玉。

問題在於:單執行緒怎麼會進入這段程式碼?按道理,但執行緒是不會出現這個情況的。

總結

這次的原始碼分析讓樓主很痛苦,網上很多的文章也無法解釋這是為什麼,希望有高人能告訴樓主,到底是怎麼回事?

相關文章