1. 程式人生 > >併發容器-ConcurrentLinkedQueue詳解

併發容器-ConcurrentLinkedQueue詳解

作者:王一飛,叩丁狼高階講師。原創文章,轉載請註明出處。     

概念

並程式設計中,一般需要用到安全的佇列,如果要自己實現安全佇列,可以使用2種方式: 方式1:加鎖,這種實現方式就是我們常說的阻塞佇列。 方式2:使用迴圈CAS演算法實現,這種方式實現佇列稱之為非阻塞佇列。 先對而已,加鎖佇列的實現較為簡單,這裡就略過,我們來重點來解讀一下非阻塞佇列。 從點到面, 下面我們來看下非阻塞佇列經典實現類:ConcurrentLinkedQueue (JDK1.8版)

ConcurrentLinkedQueue

根據API解釋,ConcurrentLinkedQueue 是一個基於連結節點的無界執行緒安全的佇列,按照先進先出原則對元素進行排序。新元素從佇列尾部插入,而獲取佇列元素,則需要從佇列頭部獲取。

看下ConcurrentLinkedQueue的結構圖外部圖

內部圖

從內圖可以瞭解ConcurrentLinkedQueue一個大概,ConcurrentLinkedQueue內部持有2個節點:head頭結點,負責出列, tail尾節點,負責入列。而元素節點Node,使用item儲存入列元素,next指向下一個元素節點。

    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
        //....
    }
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
        private transient volatile Node<E> head;
        private transient volatile Node<E> tail;  
        //....
}

ConcurrentLinkedQueue使用特點

ConcurrentLinkedQueue使用約定: 1:不允許null入列 2:在入隊的最後一個元素的next為null 3:佇列中所有未刪除的節點的item都不能為null且都能從head節點遍歷到 4:刪除節點是將item設定為null, 佇列迭代時跳過item為null節點 5:head節點跟tail不一定指向頭節點或尾節點,可能存在滯後性

之所以有這奇葩約定,全因ConcurrentLinkedQueue是併發非阻塞佇列決定的。 我們從原始碼上看一下ConcurrentLinkedQueue實現過程

入列

我們印象中連結串列特點:tail節點表示最後一個節點, head表示第一個節點。ConcurrentLinkedQueue 跟傳統的連結串列有點區別,在單執行緒環境下符合傳統連結串列特點,但涉及到多執行緒環境,ConcurrentLinkedQueue 中的tail節點不一定是最後一個節點,他可能是倒數第二個。所以ConcurrentLinkedQueue判斷隊尾條件是節點的next為null。

   public boolean offer(E e) {
        checkNotNull(e);   //為空判斷,e為null是拋異常
        final Node<E> newNode = new Node<E>(e); //將e包裝成newNode
        for (Node<E> t = tail, p = t;;) {  //迴圈cas,直至加入成功
            //t = p = tail 
            Node<E> q = p.next;
            if (q == null) {   //判斷p是否為尾節點
                //如果是,p.next = newNode
                if (p.casNext(null, newNode)) {
                    //首次新增時,p 等於t,不進行尾節點更新,所以所尾節點存在滯後性  
                    //併發環境,可能存新增/刪除,tail就更難保證正確指向最後節點。
                    if (p != t) 
                        //更新尾節點為最新元素
                        casTail(t, newNode);  
                    return true;
                }
            }
            else if (p == q)
                //當tail不執行最後節點時,如果執行出列操作,很有可能將tail也給移除了    
                //此時需要對tail節點進行復位,復位到head節點
                p = (t != (t = tail)) ? t : head;
            else
                //推動tail尾節點往隊尾移動
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

分析

初始化新增A新增B新增C

從圖上看tail不一定執行最後一個節點,但可以確定最後節點的next節點為null。到這可能朋友問他,併發環境什麼情況都有可能,ConcurrentLinkedQueue是怎麼保證執行緒安全的? 我們觀察offer方法的設計, 1:是一個死迴圈,就是不停使用cas判斷直到新增元素入隊成功。

for (Node<E> t = tail, p = t;;)

2:2個cas判斷方法 p.casNext(null, newNode) 確保佇列在入列時是原子操作

 private boolean casTail(Node<E> cmp, Node<E> val) {
     return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
 }

casTail(t, newNode); 確保tail隊尾在移動改變時是原子操作

boolean casNext(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

而在併發環境,ConcurrentLinkedQueue入列執行緒安全考慮具體可分2類: 1>執行緒1執行緒2同時入列 這個好理解, 執行緒1,執行緒2不管在offer哪個位置開始併發,他們最終的目的都是入列,也即都需要執行casNext方法, 我們只需要確保所有執行緒都有機會執行casNext方法,並且保證casNext方法是原子操作即可。casNext失敗的執行緒,可以進入下一輪迴圈,人品好的話就可以入列,衰的話繼續迴圈

2>執行緒1遍歷,執行緒2入列 ConcurrentLinkedQueue 遍歷是執行緒不安全的, 執行緒1遍歷,執行緒2很有可能進行入列出列操作, 所以ConcurrentLinkedQueue 的size是變化。換句話說,要想安全遍歷ConcurrentLinkedQueue 佇列,必須額外加鎖。

但換一個角度想, ConcurrentLinkedQueue 的設計初衷非阻塞佇列,我們更多關注入列與出列執行緒安全,這2點能保證就可以啦。

出列

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                //入列折騰的tail,那出列折騰的就是head
                E item = p.item;
                //出列判斷依據是節點的item=null
                //item != null, 並且能將操作節點的item設定null, 表示出列成功
                if (item != null && p.casItem(item, null)) {
                    if (p != h) 
                        //一旦出列成功需要對head進行移動
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    //第一輪操作失敗,下一輪繼續,調回到迴圈前
                    continue restartFromHead;
                else
                    //推動head節點移動
                    p = q;
            }
        }
    }

需求移除A

image.png移除C

看圖, 被移動的節點(item為null的節點)會被jvm回收。但是有個問題, tail也被回收, 那ConcurrentLinkedQueue就沒有tail節點了。如果此時再新增一個D元素時,會出現什麼情況?新增D

好問的朋友,又想了,ConcurrentLinkedQueue怎麼保證出列執行緒安全?道理跟之前入列一樣,cas保證原子操作即可。

總結

到這ConcurrentLinkedQueue介紹就完成了。總結下ConcurrentLinkedQueue貼點: 1>入列出列執行緒安全,遍歷不安全 2>不允許新增null元素 3>底層使用列表與cas演算法包裝入列出列安全