JUC知識點總結(七)ConcurrentLinkedQueue知識點總結

13. ConcurrentLinkedQueue (循環CAS)

應用場景:

按照適用的併發強度從低到高排列如下:

  • LinkedList/ArrayList 非線程安全,不能用於併發場景(List的方法支持棧和隊列的操作,因此可以用List封裝成stack和queue);
  • Collections.synchronizedList 使用wrapper class封裝,每個方法都用synchronized(mutex:Object)做了同步
  • LinkedBlockingQueue 採用了鎖分離的設計,避免了讀/寫操作衝突,且自動負載均衡,可以有界。BlockingQueue在生產-消費模式下首選【Iterator安全,不保證數據一致性】
  • ConcurrentLinkedQueue 適用於高併發讀寫操作,理論上有最高的吞吐量,無界,不保證數據訪問實時一致性,Iterator不拋出併發修改異常,採用CAS機制實現無鎖訪問。

綜上:

  • 在併發的場景下,如果併發強度較小,性能要求不苛刻,且鎖可控的場景下,可使用Collections.synchronizedList,既保證了數據一致又保證了線程安全,性能夠用;
  • 在大部分高併發場景下,建議使用 LinkedBlockingQueue ,性能與 ConcurrentLinkedQueue 接近,且能保證數據一致性;
  • ConcurrentLinkedQueue 適用於超高併發的場景,但是需要針對數據不一致採取一些措施。

源碼分析

offer(E e)
public boolean offer(E e) {
    checkNotNull(e);
    //創建入隊節點
    final Node<E> newNode = new Node<E>(e);
    //t爲tail節點,p爲尾節點,默認相等,採用失敗即重試的方式,直到入隊成功
    for (Node<E> t = tail, p = t; ; ) {
        //獲得p的下一個節點
        Node<E> q = p.next;
        // 如果下一個節點是null,也就是p節點就是尾節點
        if (q == null) {
            //將入隊節點newNode設置爲當前隊列尾節點p的next節點
            if (p.casNext(null, newNode)) {
                //判斷tail節點是不是尾節點,也可以理解爲如果插入結點後tail節點和p節點距離達到兩個結點
                if (p != t)
                    //如果tail不是尾節點則將入隊節點設置爲tail。
                    // 如果失敗了,那麼說明有其他線程已經把tail移動過 
                    casTail(t, newNode);
                return true;
            }
        }
        // 如果p節點等於p的next節點,則說明p節點和q節點都爲空,表示隊列剛初始化,所以返回                            head節點
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        else
            //p有next節點,表示p的next節點是尾節點,則需要重新更新p後將它指向next節點
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

即定位出尾節點=>CAS入隊=>重新定位tail節點。

poll( )
public E poll() {
    // 設置起始點  
    restartFromHead:
    for (; ; ) {
        //p表示head結點,需要出隊的節點
        for (Node<E> h = head, p = h, q; ; ) {
            //獲取p節點的元素
            E item = p.item;
            //如果p節點的元素不爲空,使用CAS設置p節點引用的元素爲null
            if (item != null && p.casItem(item, null)) {

                if (p != h) // hop two nodes at a time
                    //如果p節點不是head節點則更新head節點,也可以理解爲刪除該結點後檢查head是否與頭結點相差兩個結點,如果是則更新head節點
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            //如果p節點的下一個節點爲null,則說明這個隊列爲空,更新head結點
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //結點出隊失敗,重新跳到restartFromHead來進行出隊
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}

即獲取head節點的元素 => 判斷head節點元素是否爲空=>如果爲空,表示另外一個線程已經進行了一次出隊操作將該節點的元素取走=>如果不爲空,則使用CAS的方式將head節點的引用設置成null=>如果CAS成功,則直接返回head節點的元素=>如果CAS不成功,表示另外一個線程已經進行了一次出隊操作更新了head節點,導致元素髮生了變化,需要重新獲取head節點=>如果p節點的下一個節點爲null,則說明這個隊列爲空(此時隊列沒有元素,只有一個僞結點p),則更新head節點。

特點

  • 訪問操作採用了無鎖設計
  • Iterator的弱一致性,即不保證Iteartor訪問數據的實時一致性(與current組的成員與COW成員類似)
  • 併發offer/poll

注意事項

size操作需要遍歷整個隊列,且如果此時queue正在被修改,size可能返回不準確的數值(仍然是無法保證數據一致性),這是一個非常耗時的操作,判斷隊列是否爲空建議使用isEmpty()。如果需要保證數據一致性,頻繁獲取集合對象的size,最好不使用concurrent族的成員。

批量操作(bulk operations like addAll,removeAll,equals)無法保證原子性,因爲不保證實時性,且沒有使用獨佔鎖的設計。例如,在執行addAll的同時,有另外一個線程通過Iterator在遍歷,則遍歷的線程可能只看到一部分新增的數據。

ConcurrentLinkedQueue 沒有實現BlockingQueue接口。當隊列爲空時,take方法返回null,此時consumer會需要處理這個情況,consumer會循環調用take來保證及時獲取數據,此爲busy waiting,會持續消耗CPU資源。

與 LinkedBlockingQueue 的對比

  • LinkedBlockingQueue 採用了鎖分離的設計,put、get鎖分離,保證兩種操作的併發;
  • 當隊列爲空/滿時,某種操作會被掛起;
  • 兩者的Iterator都不不保證數據一致性,Iterator遍歷的是Iterator創建時已存在的節點,創建後的修改不保證能反應出來。
  • LinkedBlockingQueue 的size是在內部用一個AtomicInteger保存,執行size操作直接獲取此原子量的當前值,時間複雜度O(1)。
    ConcurrentLinkedQueue 的size操作需要遍歷(traverse the queue),因此比較耗時,時間複雜度至少爲O(n),建議使用isEmpty()。

下一篇
JUC知識點總結(八)CopyOnWrite機制及其在JAVA中的實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章