ConcurrentLinkedQueue原理(上)

來源:互聯網
上載者:User

ConcurrentLinkedQueue是Queue的一個安全執行緒實現。

它是一個基於連結節點的無界安全執行緒隊列。此隊列按照 FIFO(先進先出)原則對元素進行排序。隊列的頭部 是隊列中時間最長的元素。

隊列的尾部 是隊列中時間最短的元素。新的元素插入到隊列的尾部,隊列擷取操作從隊列頭部獲得元素。

當多個線程共用訪問一個公用 collection 時,ConcurrentLinkedQueue 是一個恰當的選擇。此隊列不允許使用 null 元素。

我來分析設計一個安全執行緒的隊列哪幾種方法。
第一種:使用synchronized同步隊列,就像Vector或者Collections.synchronizedList/Collection那樣。

 顯然這不是一個好的並發隊列,這會導致輸送量急劇下降。
第二種:使用Lock。一種好的實現方式是使用ReentrantReadWriteLock來代替ReentrantLock提高讀取的輸送量。

 但是顯然 ReentrantReadWriteLock的實現更為複雜,而且更容易導致出現問題,

 另外也不是一種通用的實現方式,因為 ReentrantReadWriteLock適合哪種讀取量遠遠大於寫入量的場合。

 當然了ReentrantLock是一種很好的實現,結合 Condition能夠很方便的實現阻塞功能,

 這在後面介紹BlockingQueue的時候會具體分析。
第三種:使用CAS操作。儘管Lock的實現也用到了CAS操作,但是畢竟是間接操作,而且會導致線程掛起。

 一個好的並發隊列就是採用某種非阻塞演算法來取得最大的輸送量。

 ConcurrentLinkedQueue採用的就是第三種策略。

 它採用了參考資料1(http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf) 中的演算法。
要使用非阻塞演算法來完成隊列操作,那麼就需要一種“迴圈嘗試”的動作,就是迴圈操作隊列,直到成功為止,失敗就會再次嘗試。

針對各種功能深入分析。

先介紹下ConcurrentLinkedQueue的資料結構。

ConcurrentLinkedQueue只有頭結點、尾節點兩個元素,而對於一個節點Node而言除了儲存隊列元素item外,還有一個指向下一個節點的引用next。

看起來整個資料結構還是比較簡單的。但是也有幾點是需要說明:

   1. 所有結構(head/tail/item/next)都是volatile類型。 這是因為ConcurrentLinkedQueue是非阻塞的,

   所以只有volatile才能使變數的寫操作對後續讀操作是可見的(這個是有 happens-before法則保證的)。同樣也不會導致指令的重排序。

   2. 所有結構的操作都帶有原子操作,這是由AtomicReferenceFieldUpdater保證的,

   這在原子操作中介紹過。它能保證需要的時候對變數的修改操作是原子的。

   3. 由於隊列中任何一個節點(Node)只有下一個節點的引用,所以這個隊列是單向的,根據FIFO特性,也就是說出隊列在頭部(head),入隊列在尾部(tail)。

   頭部儲存有進入隊列最長時間的元素,尾部是最近進入的元素。

   4. 沒有對隊列長度進行計數,所以隊列的長度是無限的,同時擷取隊列的長度的時間不是固定的,這需要遍曆整個隊列,並且這個計數也可能是不精確的。

   5. 初始情況下隊列頭和隊列尾都指向一個空節點,但是非null,這是為了方便操作,不需要每次去判斷head/tail是否為空白。但是head卻不作為存取元素的節點,

   tail在不等於head情況下儲存一個節點元素。也就是說head.item這個應該一直是空,但是tail.item卻不一定是空(如果 head!=tail,那麼tail.item!=null)。

對於第5點,可以從ConcurrentLinkedQueue的初始化中看到。這種頭結點也叫“偽節點”,也就是說它不是真正的節點,只是一標識,就像c中的字元數組後面的\0以後,只是用來標識結束,並不是真正字元數組的一部分。

    private transient volatile Node<E> head = new Node<E>(null, null);

    private transient volatile Node<E> tail = head;

有了上述5點再來解釋相關API操作就容易多了。

在上一節中列出了add/offer/remove/poll/element/peek等價方法的區別,所以這裡就不再重複了。

清單1 入隊列操作
    public boolean offer(E e) {

        if (e == null) throw new NullPointerException();

        Node<E> n = new Node<E>(e, null);

        for (;;) {

            Node<E> t = tail;

            Node<E> s = t.getNext();

            if (t == tail) {

                if (s == null) {

                    if (t.casNext(s, n)) {

                        casTail(t, n);

                        return true;

                    }

                } else {

                    casTail(t, s);

                }

            }

        }

    }

清單1 描述的是入隊列的過程。整個過程是這樣的。

         1. 擷取尾節點t,以及尾節點的下一個節點s。如果尾節點沒有被別人修改,也就是t==tail,進行2,否則進行1。

         2. 如果s不為空白,也就是說此時尾節點後面還有元素,那麼就需要把尾節點往後移,進行1。否則進行3。

         3. 修改尾節點的下一個節點為新節點,如果成功就修改尾節點,返回true。否則進行1。

從操作3中可以看到是先修改尾節點的下一個節點,然後才修改尾節點位置的,所以這才有操作2中為什麼擷取到的尾節點的下一個節點不為空白的原因。

特別需要說明的是,對尾節點的tail的操作需要換成臨時變數t和s,一方面是為了去掉volatile變數的可變性,另一方面是為了減少volatile的效能影響。

 

清單2 描述的出隊列的過程,這個過程和入隊列相似,有點意思。

頭結點是為了標識隊列起始,也為了減少null 指標的比較,所以頭結點總是一個item為null的非null節點。

也就是說head!=null並且 head.item==null總是成立。所以實際上擷取的是head.next,

一旦將頭結點head設定為head.next成功就將新head的 item設定為null。至於以前就的頭結點h,h.item=null並且h.next為新的head,

但是由於沒有對h的引用,所以最終會被GC回收。這就是整個出隊列的過程。

清單2 出隊列操作
    public E poll() {

        for (;;) {

            Node<E> h = head;

            Node<E> t = tail;

            Node<E> first = h.getNext();

            if (h == head) {

                if (h == t) {

                    if (first == null)

                        return null;

                    else

                        casTail(t, first);

                } else if (casHead(h, first)) {

                    E item = first.getItem();

                    if (item != null) {

                        first.setItem(null);

                        return item;

                    }

                    // else skip over deleted item, continue loop,

                }

            }

        }

    }

 

另外對於清單3 描述的擷取隊列大小的過程,由於沒有一個計數器來對隊列大小計數,所以擷取隊列的大小隻能通過從頭到尾完整的遍曆隊列,顯然這個代價是很大的。所以通常情況下ConcurrentLinkedQueue需要和一個AtomicInteger搭配才能擷取隊列大小。後面介紹的BlockingQueue正是使用了這種思想。

清單3 遍曆隊列大小
    public int size() {

        int count = 0;

        for (Node<E> p = first(); p != null; p = p.getNext()) {

            if (p.getItem() != null) {

                // Collections.size() spec says to max out
                if (++count == Integer.MAX_VALUE)
                    break;
            }
        }
        return count;
    }
注意1:關於ConcurrentLinkedQueue原理更多可參考《ConcurrentLinkedQueue原理(下)
注意2:關於ConcurrentLinkedQueue的API介紹可參考《ConcurrentLinkedQueue

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.