Java并發編程(七)ConcurrentLinkedQueue的實現原理和源碼分析

相關文章
Java并發編程(一)線程定義、狀態和屬性
Java并發編程(二)同步
Java并發編程(三)volatile域
Java并發編程(四)Java內存模型
Java并發編程(五)ConcurrentHashMap的實現原理和源碼分析
Java并發編程(六)阻塞隊列

前言

我們要實現一個線程安全的隊列有兩種實現方式一種是使用阻塞算法,另一種是使用非阻塞算法。使用阻塞算法的隊列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現,而非阻塞的實現方式則可以使用循環CAS的方式來實現,本節我們就來研究下ConcurrentLinkedQueue是如何保證線程安全的同時又能高效的操作的。

1.ConcurrentLinkedQueue的結構

ConcurrentLinkedQueue是一個基于鏈接節點的無界線程安全隊列,它采用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。基于CAS的“wait-free”(常規無等待)來實現,CAS并不是一個算法,它是一個CPU直接支持的硬件指令,這也就在一定程度上決定了它的平臺相關性。

當前常用的多線程同步機制可以分為下面三種類型:

  • volatile 變量:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性保證,不提供原子性。
  • CAS 原子指令:輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性和原子化更新保證。
  • 互斥鎖:重量級多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性和原子性。

ConcurrentLinkedQueue 的非阻塞算法實現主要可概括為下面幾點:

  • 使用 CAS 原子指令來處理對數據的并發訪問,這是非阻塞算法得以實現的基礎。
  • head/tail 并非總是指向隊列的頭 / 尾節點,也就是說允許隊列處于不一致狀態。 這個特性把入隊 /
    出隊時,原本需要一起原子化執行的兩個步驟分離開來,從而縮小了入隊 /
    出隊時需要原子化更新值的范圍到唯一變量。這是非阻塞算法得以實現的關鍵。
  • 以批處理方式來更新head/tail,從整體上減少入隊 / 出隊操作的開銷。

ConcurrentLinkedQueue由head節點和tail節點組成,每個節點(Node)由節點元素(item)和指向下一個節點的引用(next)組成,節點與節點之間就是通過這個next關聯起來,從而組成一張鏈表結構的隊列。默認情況下head節點存儲的元素為空,tail節點等于head節點。

2.入隊列

入隊列就是將入隊節點添加到隊列的尾部,假設我們要在一個隊列中依次插入4個節點,來看看下面的圖來方便理解:


  • 添加元素1。隊列更新head節點的next節點為元素1節點。又因為tail節點默認情況下等于head節點,所以它們的next節點都指向元素1節點。
  • 添加元素2。隊列首先設置元素1節點的next節點為元素2節點,然后更新tail節點指向元素2節點。
  • 添加元素3,設置tail節點的next節點為元素3節點。
  • 添加元素4,設置元素3的next節點為元素4節點,然后將tail節點指向元素4節點。

入隊主要做兩件事情,第一是將入隊節點設置成當前隊列尾節點的下一個節點。第二是更新tail節點,在入隊列前如果tail節點的next節點不為空,則將入隊節點設置成tail節點,如果tail節點的next節點為空,則將入隊節點設置成tail的next節點,所以tail節點不總是尾節點。

上面的分析從單線程入隊的角度來理解入隊過程,但是多個線程同時進行入隊情況就變得更加復雜,因為可能會出現其他線程插隊的情況。如果有一個線程正在入隊,那么它必須先獲取尾節點,然后設置尾節點的下一個節點為入隊節點,但這時可能有另外一個線程插隊了,那么隊列的尾節點就會發生變化,這時當前線程要暫停入隊操作,然后重新獲取尾節點。讓我們再通過源碼來詳細分析下它是如何使用CAS方式來入隊的(JDK1.8):

    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指令將入隊節點設置成尾節點的next節點,如果不成功則重試;第三是重新定位tail節點。
從第一個if判斷就來判定p有沒有next節點如果沒有則p是尾節點則將入隊節點設置為p的next節點,同時如果tail節點不是尾節點則將入隊節點設置為tail節點。如果p有next節點則p的next節點是尾節點,需要重新更新p后將它指向next節點。還有一種情況p等于p的next節點說明p節點和p的next節點都為空,表示這個隊列剛初始化,正準備添加數據,所以需要返回head節點。

3.出隊列

出隊列的就是從隊列里返回一個節點元素,并清空該節點對元素的引用。讓我們通過每個節點出隊的快照來觀察下head節點的變化。



從上圖可知,并不是每次出隊時都更新head節點,當head節點里有元素時,直接彈出head節點里的元素,而不會更新head節點。只有當head節點里沒有元素時,出隊操作才會更新head節點。讓我們再通過源碼來深入分析下出隊過程(JDK1.8):

    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節點的updateHead方法:

final void updateHead(Node<E> h, Node<E> p) 
{
     // 如果兩個結點不相同,嘗試用CAS指令原子更新head指向新頭節點
     if (h != p && casHead(h, p))
         //將舊的頭結點指向自身以實現刪除
     h.lazySetNext(h);
}

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

4.隊列判空

有些人在判斷隊列是否為空時喜歡用queue.size()==0,讓我們來看看size方法:

public int size() 
{
     int count = 0;
     for (Node<E> p = first(); p != null; p = succ(p))
         if (p.item != null)
             // Collection.size() spec says to max out
             if (++count == Integer.MAX_VALUE)
                 break;
     return count;
 }

可以看到這樣在隊列在結點較多時會依次遍歷所有結點,這樣的性能會有較大影響,因而可以考慮empty函數,它只要判斷第一個結點(注意不一定是head指向的結點)。

  public boolean isEmpty() {
        return first() == null;
    }

參考資料
《Java并發編程的藝術》
JDK1.8源碼
http://www.zsfblues.com/2016/06/15/ConcurrentLinkedQueue%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容