Java並發中的ConcurrentLinkedQueue源碼分析__java

來源:互聯網
上載者:User

本文中多次提到CAS演算法,先做個CAS演算法的簡單描述 CAS(非阻塞演算法)說明

CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。若且唯若預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。
ConcurrentLinkedQueue是一種安全執行緒的隊列。他是使用非阻塞演算法(CAS)來實現安全執行緒的。ConcurrentLinkedQueue是一個基於連結節點的無界安全執行緒隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部;當我們擷取一個元素時,它會返回隊列頭部的元素。 ConcurrentLinkedQueue不變式

在後面的原始碼分析中,我們將會看到隊列有時會處於不一致狀態。為此,ConcurrentLinkedQueue 使用三個不變式 ( 基本不變式,head 的不變式和 tail 的不變式 ),來約束隊列中方法的執行。通過這三個不變式來維護非阻塞演算法的正確性 基本不變式

在執行方法之前和之後,隊列必須要保持的不變式:
當入隊插入新節點之後,隊列中有一個 next 域為 null 的(最後)節點。
從 head 開始遍曆隊列,可以訪問所有 item 域不為 null 的節點。 head 的不變式和可變式

在執行方法之前和之後,head 必須保持的不變式:
所有“活著”的節點(指未刪除節點),都能從 head 通過調用 succ() 方法遍曆可達。
head 不能為 null。
head 節點的 next 域不能引用到自身。
在執行方法之前和之後,head 的可變式:
head 節點的 item 域可能為 null,也可能不為 null。
允許 tail 滯後(lag behind)於 head,也就是說:從 head 開始遍曆隊列,不一定能到達 tail。 tail 的不變式和可變式

在執行方法之前和之後,tail 必須保持的不變式:
通過 tail 調用 succ() 方法,最後節點總是可達的。
tail 不能為 null。
在執行方法之前和之後,tail 的可變式:
tail 節點的 item 域可能為 null,也可能不為 null。
允許 tail 滯後於 head,也就是說:從 head 開始遍曆隊列,不一定能到達 tail。
tail 節點的 next 域可以引用到自身。
在接下來的原始碼分析中,在初始化 ConcurrentLinkedQueue 之後及調用入隊 / 出隊方法之前和之後,我們都會參照上面三個不變式來分析它們的正確性。 ConcurrentLinkedQueue結構

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>        implements Queue<E>, java.io.Serializable

ConcurrentLinkedQueue由head節點和tail節點群組成,每個節點(Node)由節點元素(item)和指向下一個節點(next)的引用組成,節點與節點之間就是通過這個next關聯起來,從而組成一張鏈表結構的隊列。預設情況下head節點儲存的元素為空白,tail節點等於head節點 節點類實現及隊列初始化 節點類定義

ConcurrentLinkedQueue 是用節點連結成的鏈表來實現的。首先,讓我們來看看節點類的原始碼

private static class Node<E> {        private volatile  E item;           // 聲明為 volatile 型       private volatile  Node<E> next;    // 聲明為 volatile 型       Node(E item) {                       // 建立新節點           lazySetItem(item);              // 惰性設定 item 域的值        }        E getItem() {            return item;        }        boolean casItem(E cmp, E val) {   // 使用 CAS 指令設定 item 域的值           return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);        }        void setItem(E val) {  // 使用“volatile 寫”的方式,設定 item 域的值            item = val;        }        voidlazySetItem(E val) { //惰性設定 item 域的值                UNSAFE.putOrderedObject(this, itemOffset, val);        }        void lazySetNext(Node<E> val) {    // 惰性設定 next 域的值            UNSAFE.putOrderedObject(this, nextOffset, val);        }        Node<E> getNext() {            return next;        }                                                      //CAS 設定 next 域的值       boolean casNext(Node<E> cmp, Node<E> val) {            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);        }        private static final sun.misc.Unsafe UNSAFE=     // 域更新器       sun.misc.Unsafe.getUnsafe();        private static final long nextOffset=              //next 域的位移量       objectFieldOffset(UNSAFE, "next", Node.class);        private static final long itemOffset=              //item 域的位移量       objectFieldOffset(UNSAFE, "item", Node.class);  }

在 ConcurrentLinkedQueue 的實際應用中,會頻繁分配大量生命週期短暫的節點對象。為了降低開銷,Node 類的 item 域和 next 域被聲明為普通的 volatile 類型。它們通過原子參考網域更新器(AtomicReferenceFieldUpdater),使用反射來更新 節點類型說明

有效節點:從 head 向後遍曆可達的節點當中,item 域不為 null 的節點。
無效節點:從 head 向後遍曆可達的節點當中,item 域為 null 的節點。
以刪除節點:從 head 向後遍曆不可達的節點。
哨兵節點:連結到自身的節點(哨兵節點同時也是以刪除節點)。
前端節點:隊列中的第一個有效節點(如果有的話)。
尾節點:隊列中 next 域為 null 的節點(可以是無效節點)。
如下圖所示:

對比 head 的不變式和 tail 的不變式可以看出,head 只能指向有效節點和無效節點,而 tail 可以指向任意節點,包括以刪除節點和哨兵節點。在 ConcurrentLinkedQueue 中,入隊時只能把新節點連結到尾節點的後面,出隊時只能刪除前端節點 入隊列 入隊列過程

入隊列就是將入隊節點添加到隊列的尾部,在 ConcurrentLinkedQueue 中,插入新節點時,不用考慮尾節點是否為有效節點,直接把新節點插入到尾節點的後面即可。由於 tail 可以指向任意節點,所以入隊時必須先通過 tail 找到尾節點,然後才能執行插入操作。如果插入不成功(說明其他線程已經搶先插入了一個新的節點)就繼續向後推進。重複上述迭代過程,直到插入成功為止。
下面讓我們看看入隊的源碼

public boolean offer(E e) {        if (e == null)            throw new NullPointerException();        // 入隊前建立一個入隊新節點        Node<E> n = new Node<E>(e); // 建立新節點        // 死迴圈,入隊不成功反覆入隊。        retry: for (;;) {            // 建立一個指向tail節點的引用            Node<E> t = tail;            // p用來表示隊列的尾節點,預設情況下等於tail節點            Node<E> p = t;            for (int hops = 0;; hops++) {                // 找到 tail 的下一個節點 next                Node<E> next = succ(p);                // next節點不為空白,說明p不是尾節點,需要更新p後在將它指向next節點                if (next != null) {                     // 如果已經至少越過了兩個節點,且 tail 被修改 (tail 被修改,說明其他線程向隊列添加了新的節點,且更新 tail 成功 ),                    // 並且當前節點還是不等於尾節點                    if (hops > HOPS && t != tail)                         // 跳出內外兩層迴圈,重新開始迭代(因為 tail 剛剛被其他線程更新了)                        continue retry; // B2                    // 向後推進到下一個節點                    p = next; // B3                    // 如果p是尾節點,則設定p節點的next節點為入隊節點                } else if (p.casNext(null, n)) { // C                    // 如果tail節點有大於等於1個next節點,則將入隊節點設定成tail節點,                    // 更新失敗了也沒關係,因為失敗了表示有其他線程成功更新了tail節點                    if (hops >= HOPS) // C1                        // 使用 CAS 原子指令更新 tail 指向這個新插入的節點,允許失敗                        casTail(t, n); // C2                    return true; // C3                } else {// p有next節點,表示p的next節點是尾節點,則重新設定p節點                    p = succ(p); // D1                }            }        }    }

從原始碼角度來看,整個入隊過程主要做兩件事情:第一是定位出尾節點;第二是使用CAS演算法將入隊節點設定成尾節點的next節點,如不成功則重試 定位尾節點

tail節點並不總是尾節點,所以每次入隊都必須先通過tail節點來找到尾節點。尾節點可能是tail節點,也可能是tail節點的next節點。代碼中迴圈體中的第一個if就是判斷tail是否有next節點,有則表示next節點可能是尾節點。擷取tail節點的next節點需要注意的是p節點等於p的next節點的情況,只有一種可能就是p節點和p的next節點都等於空,表示這個隊列剛初始化,正準備添加節點,所以需要返回head節點
擷取p節點的next節點源碼如下:

 /**     * Returns the successor of p, or the first node if p.next has been     * linked to self, which will only be true if traversing with a     * stale pointer that is now off the list.     */    final Node<E> succ(Node<E> p) {        // TODO: should we skip deleted nodes here?        Node<E> q = p.next;        return (p == q) ? first() : q;    }

下面針對tail可能出現的情況分別通過圖示說明
1、tail指向尾節點

開始時,tail 指向 D 節點,首先尋找 D 節點的後繼節點。由於 D 的後繼節點為 null,所以插入新節點到 D 節點的後面。如果插入成功就退出方法;如果插入失敗(說明其他線程剛剛插入了一個新節點),就向後推進到新插入的節點,然後重新開始迭代。下圖是插入成功後的示意圖:

在上圖中,由於 tail 滯後於尾節點的節點數還沒有達到 HOPS 指定的閾值,所以 tail 沒有被更新。
2、tail指向非尾節點

開始時,tail 指向 C 節點。首先找到 C 的後繼節點 D,然後向後推進到節點 D,後面代碼執行路徑與上面的“tail 指向尾節點 ”的代碼執行路徑相同。下圖是插入成功後的結構示意圖:

上圖中的 tail 更新了位置。因為在添加 E 節點後,tail 滯後的節點數達到了 HOPS 指定的閾值。這觸發執行更新 tail 的 CAS 操作。

private static final int HOPS = 1;

3、tail滯後於head

開始時,tail 指向 A 節點。首先找到 A 的後繼節點 B,然後向後推進到節點 B。由於 B 是哨兵節點,產生跳轉動作,跳過 C 節點,從 head 指向的 D 節點開始繼續向後遍曆。後面的代碼執行路徑與“tail 指向非尾節點”相同。下面是成功插入一個新節點後的結構示意圖
出隊列

出隊列的就是從隊列裡返回一個節點元素,並清空該節點對元素的引用。並不是每次出隊時都更新head節點,當head節點裡有元素時,直接彈出head節點裡的元素,而不會更新head節點。只有當head節點裡沒有元素時,出隊操作才會更新head節點。這種做法也是通過hops變數來減少使用CAS更新head節點的消耗,從而提高出隊效率。
下面我看下出隊的源碼實現:

    public E poll() {         Node<E> h = head;         // p表示前端節點,需要出隊的節點        Node<E> p = h;         for (int hops = 0; ; hops++) {             // 擷取p節點的元素            E item = p.getItem();             // 如果p節點的元素不為空白,使用CAS設定p節點引用的元素為null            if (item != null && p.casItem(item, null)) {                // 如果迭代過程已經越過了不小於 1 個節點也就是                if (hops >= HOPS) {                     // 將p節點下一個節點設定成head節點                     Node<E> q = p.getNext();                     // 如果 q 不為 null,設定 head 指向後繼節點 q;否則設定 head 指向當前節點 p(此時隊列為空白,只有一個偽節點 p)                     updateHead(h, (q != null) ? q : p);                              }                 // 返回被移除節點 item 域的值                return item;                                                    }            // 如果前端節點的元素為空白或前端節點發生了變化,這說明前端節點已經被另外            // 一個線程修改了。那麼擷取p節點的下一個節點            Node<E> next = succ(p);             // 如果p的下一個節點也為空白,說明這個隊列已經空了             if (next == null) {                 // 設定 head 指向 p 節點(此時隊列為空白,只有一個偽節點 p)                 updateHead(h, p);                // 退出迴圈                 break;                                                            }             // 如果下一個元素不為空白,則將前端節點的下一個節點設定成前端節點            p = next;                                                        }         return null;     }

首先擷取前端節點的元素,然後判斷前端節點元素是否為空白,如果為空白,表示另外一個線程已經進行了一次出隊操作將該節點的元素取走,如果不為空白,則使用CAS的方式將前端節點的引用設定成null,如果CAS成功,則直接返回前端節點的元素,如果不成功,表示另外一個線程已經進行了一次出隊操作更新了head節點,導致元素髮生了變化,需要重新擷取前端節點。 前端節點定位

根據 head 的不變式和可變式,在執行出隊操作前,head 在隊列中的位置共有兩種可能:
head 指向有效節點。
head 指向無效節點。
1、head指向有效節點

出隊時,首先取得 head 指向的 A 節點的 item 域的值,然後通過 CAS 設定 A 節點 item 域的值為 null。如果成功,由於此時越過的節點數為 0,所以直接返回 A 節點 item 域原有的值。如果不成功,說明其他線程已經搶先刪除了該節點,此時向後推進到 B 節點。重複這個過程,直到成功刪除一個節點;如果遍曆完隊列也沒有刪除成功,則返回 null。下面是成功刪除後的結構示意圖:

在上圖中,雖然 A 節點被設定成無效節點,但 head 依然指向它,因為刪除操作越過的節點數還沒有達到 HOPS 指定的閥值。
接下來,讓我們來看看第二種情形的結構示意圖
2、head指向無效節點

首先獲得 head 指向節點的 item 域的值,由於為 null,所以向後推進到 B 節點。獲得 B 節點 item 域的值後,通過 CAS 設定該值為 null。如果成功,由於已經達到 HOPS 指定的閥值,觸發執行 head 更新。如果不成功(說明其他線程已經搶先刪除了 B 節點),繼續向後推進到 C 節點。重複這個過程,直到刪除一個有效節點。如果遍曆完隊列也沒有刪除成功,則返回 null。下圖是成功刪除後的結構示意圖:

從上圖我們可以看到,在執行刪除操作過程中,head 越過的節點數達到閥值,觸發執行 head 的更新,使它指向 C 節點。
從上面刪除前端節點後的兩個結構示意圖可以看出,執行出隊操作後的隊列依然滿足三個不變式。 總結

ConcurrentLinkedQueue 的非阻塞演算法實現非常精巧,也非常複雜。它使用 CAS 原子指令來處理對資料的並發訪問。同時,它允許隊列處於不一致狀態。這個特性分離了入隊 / 出隊操作中包含的兩個需要一起原子執行的步驟,從而有效縮小了入隊 / 出隊時的原子化(更新值的)範圍為唯一變數。由於隊列可能處於不一致狀態,為此 ConcurrentLinkedQueue 使用三個不變式來維護非阻塞演算法的正確性。
參考書籍:
《Java並發編程》、JDK源碼

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.