並發編程之 ConcurrentLinkedQueue 源碼剖析


前言

今天我們繼續分析 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) 這行代碼,樓主分析的沒有問題,但是再樓主的單線程測試這段代碼時,出現了詭異的情況,無法解釋,因此, 樓主貼出測試用例,大家一起看看:

測試代碼:

斷點代碼:

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

問題在於:單線程怎么會進入這段代碼?按道理,但線程是不會出現這個情況的。

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM