JUC源碼分析-集合篇(五):ConcurrentLinkedDeque

ConcurrentLinkedDeque 是雙向鏈表結構的無界并發隊列。從JDK 7開始加入到J.U.C的行列中。使用CAS實現并發安全,與 ConcurrentLinkedQueue 的區別是該阻塞隊列同時支持FIFOFILO兩種操作方式,即可以從隊列的頭和尾同時操作(插入/刪除)。適合“多生產,多消費”的場景。內存一致性遵循對 ConcurrentLinkedDeque 的插入操作先行發生于(happen-before)訪問或移除操作。相較于 ConcurrentLinkedQueue,ConcurrentLinkedDeque 由于是雙端隊列,所以在操作和概念上會更加復雜。

概述

ConcurrentLinkedDeque(后面稱CLD) 的實現方式繼承了 ConcurrentLinkedQueue 和 LinkedTransferQueue的思想,在非阻塞算法的實現方面與 ConcurrentLinkedQueue 基本一致。關于 ConcurrentLinkedQueue,請參考筆者的上一篇文章:JUC源碼分析-集合篇(三):ConcurrentLinkedQueue。

數據結構

ConcurrentLinkedDeque 繼承關系

重要屬性:

//頭節點
private transient volatile Node<E> head;
//尾節點
private transient volatile Node<E> tail;
//終止節點
private static final Node<Object> PREV_TERMINATOR, NEXT_TERMINATOR;
//移除節點時更新鏈表屬性的閥值
private static final int HOPS = 2;

和ConcurrentLinkedQueue一樣,CLD 內部也只維護了headtail屬性,對 head/tail 節點也使用了“不變性”和“可變性”約束,不過跟 ConcurrentLinkedQueue 有些許差異,我們來看一下:

head/tail 的不變性:

  1. 第一個節點總是能以O(1)的時間復雜度從 head 通過 prev 鏈接到達;
  2. 最后一個節點總是能以O(1)的時間復雜度從 tail 通過 next 鏈接到達;
  3. 所有live節點(item不為null的節點),都能從第一個節點通過調用 succ() 方法遍歷可達;
  4. 所有live節點(item不為null的節點),都能從最后一個節點通過調用 pred() 方法遍歷可達;
  5. head/tail 不能為 null;
  6. head 節點的 next 域不能引用到自身;
  7. head/tail 不會是GC-unlinked節點(但它可能是unlink節點)。

head/tail的可變性:

  1. head/tail 節點的 item 域可能為 null,也可能不為 null;
  2. head/tail 節點可能從first/last/tail/head 節點訪問時不可達;
  3. tail 節點的 next 域可以引用到自身。

注:CLD中也對 head/tail 的更新也使用了“松弛閥值”的概念(在 ConcurrentLinkedQueue 一篇中已經分析),除此之外,CLD設定了一個“跳躍閥值”-HOPS(指在查找活動節點時跳過的已刪除節點數),在執行出隊操作時,跳躍節點數大于2或者操作的節點不是 first/last 節點時才會更新鏈表(后面源碼中詳細分析)。

除此之外,再來看看CLD中另外兩個屬性:

  • PREV_TERMINATOR:prev的終止節點,next指向自身,即PREV_TERMINATOR.next = PREV_TERMINATOR。在 first 節點出列后,會把first.next指向自身(first.next=first),然后把prev設為PREV_TERMINATOR。
  • NEXT_TERMINATOR:next的終止節點,prev指向自身,即NEXT_TERMINATOR.pre = NEXT_TERMINATOR。在 last 節點出列后,會把last.prev指向自身(last.prev=last),然后把next設為
    NEXT_TERMINATOR。

源碼解析

在開始源碼分析之前,我們先來看一下CLD中對Node的定義:

  1. live node:節點的 item!=null 被稱為live節點。當節點的 item 被 CAS 改為 null,邏輯上來講這個節點已經從鏈表中移除;一個新的元素通過 CAS 添加到一個包含空 prev 或空 next 的 first 或 last 節點,這個元素的節點在這時是 live節點。
  2. first node & last node:首節點(first node)總會有一個空的 prev 引用,終止任何從 live 節點開始的 prev 引用鏈;同樣的最后一個節點(last node)是 next 的終止節點。first/last 節點的 item 可以為 null。并且 first 和 last 節點總是相互可達的。
  3. active node:live節點、first/last 節點也被稱為活躍節點(active node),活躍節點一定是被鏈接的,如果p節點為active節點,則:p.item != null || (p.prev == null && p.next != p) || (p.next == null && p.prev != p)
  4. self-node:自鏈接節點,prev 或 next 指向自身的節點,自鏈接節點用在解除鏈接操作中,并且它們都不是active node。
  5. head/tail節點:head/tail 也可能不是 first/last 節點。從 head 節點通過 prev 引用總是可以找到 first 節點,從 tail 節點通過 next 引用總是可以找到 last 節點。允許 head 和 tail 引用已刪除的節點,這些節點沒有鏈接,因此可能無法從 live 節點訪問到。

節點刪除時經歷三個階段:邏輯刪除(logical deletion),未鏈接( unlinking),和gc未鏈接( gc-unlinking):

  • logical deletion:通過 CAS 修改節點 item 為 null 來完成,表示當前節點可以被解除鏈接(unlinking)。

  • unlinking: 這種狀態下的節點與其他 active 節點有鏈接,但是其他 active 節點與之都沒有鏈接,也就是說從這個狀態下的節點可以達到 active 節點,但是從 active 節點不可達到這種狀態的節點。在任何時候,從 first 通過 next 找到的 live 節點和從 last 通過 prev 找到的節點總是相等的。但是,在節點被邏輯刪除時上述結論不成立,這些被邏輯刪除的節點也可能只從一端是可達的。

  • gc-unlinking: GC未鏈接使已刪除節點不可達到 active 節點,使GC更容易回收被刪除的節點。通過讓節點自鏈接或鏈接到終止節點(PREV_TERMINATOR 或 NEXT_TERMINATOR)來實現。 gc-unlinking 節點從 head/tail 訪問不可達。這一步是為了使數據結構保持GC健壯性(gc-robust),防止保守式GC(conservative GC,目前已經很少使用)對這些邊界空間的使用。對保守式GC來說,使數據結構保持GC健壯性會消除內存無限滯留的問題,同時也提高了分代收機器的性能。

如果同學們對上述理論還是一頭霧水,那么從源碼解析中我們就可以直觀的看到它們的作用。
OK!準備工作已經做完,下面我們正式開始源碼解析。


添加(入列)

CLD的添加方法包括:offer(E)、add(E)、push(E)、addFirst(E)、addLast(E)、offerFirst(E)、offerLast(E),所有這些操作都是通過linkFirst(E)linkLast(E)來實現的。

linkFirst(E) / linkLast(E)

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    restartFromHead:
    for (;;)
        //從head節點往前尋找first節點
        for (Node<E> h = head, p = h, q;;) {
            if ((q = p.prev) != null &&
                (q = (p = q).prev) != null)
                // Check for head updates every other hop.
                // If p == q, we are sure to follow head instead.
                //如果head被修改,返回head重新查找
                p = (h != (h = head)) ? h : q;
            else if (p.next == p) // 自鏈接節點,重新查找
                continue restartFromHead;
            else {
                // p is first node
                newNode.lazySetNext(p); // CAS piggyback
                if (p.casPrev(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this deque,
                    // and for newNode to become "live".
                    if (p != h) // hop two nodes at a time 跳兩個節點時才修改head
                        casHead(h, newNode);  // Failure is OK.
                    return;
                }
                // Lost CAS race to another thread; re-read prev
            }
        }
}

/**
 * Links e as last element.
 */
private void linkLast(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    restartFromTail:
    for (;;)
        //從tail節點往后尋找last節點
        for (Node<E> t = tail, p = t, q;;) {
            if ((q = p.next) != null &&
                (q = (p = q).next) != null)
                // Check for tail updates every other hop.
                // If p == q, we are sure to follow tail instead.
                //如果tail被修改,返回tail重新查找
                p = (t != (t = tail)) ? t : q;
            else if (p.prev == p) // 自鏈接節點,重新查找
                continue restartFromTail;
            else {
                // p is last node
                newNode.lazySetPrev(p); // CAS piggyback
                if (p.casNext(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this deque,
                    // and for newNode to become "live".
                    if (p != t) // hop two nodes at a time 跳兩個節點時才修改tail
                        casTail(t, newNode);  // Failure is OK.
                    return;
                }
                // Lost CAS race to another thread; re-read next
            }
        }
}

說明linkFirst是插入新節點到隊列頭的主函數,執行流程如下:
首先從 head 節點開始向前循環找到 first 節點(p.prev==null&&p.next!=p);然后通過lazySetNext設置新節點的 next 節點為 first;然后 CAS 修改 first 的 prev 為新節點。注意這里 CAS 指令成功后會判斷 first 節點是否已經跳了兩個節點,只有在跳了兩個節點才會 CAS 更新 head,這也是為了節省 CAS 指令執行開銷。linkLast是插入新節點到隊列尾,執行流程與linkFirst一致,不多贅述,具體見源碼。
注:lazySetNext通過 Unsafe 類的putOrderedObject實現,有關這個方法,請參考筆者的另一篇文章:JUC源碼分析—CAS和Unsafe。


獲取(出列)

CLD的獲取方法分兩種:
獲取節點:peek、peekFirst 、peekLast、getFirst、getLast,都是通過peekFirst 、peekLast實現。
獲取并移除節點: poll、pop、remove、pollFirst、pollLast、removeFirst、removeLast,都是通過pollFirst、pollLast實現。

pollFirst、pollLast包括了peekFirst 、peekLast的實現,都是找到并返回 first/last 節點,不同的是,pollFirst、pollLastpeekFirst 、peekLast多了 unlink 這一步。所以這里我們只對pollFirstpollLast兩個方法進行解析。
首先來看一下pollFirst() :

pollFirst()

/**獲取并移除隊列首節點*/
public E pollFirst() {
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null && p.casItem(item, null)) {
            unlink(p);
            return item;
        }
    }
    return null;
}

說明: pollFirst()用于找到鏈表中首個 item 不為 null 的節點(注意并不是first節點,因為first節點的item可以為null),并返回節點的item。涉及的內部方法較多,不過都很簡單,我們通過穿插代碼方式分析:

  1. 首先通過first()方法找到 first 節點,first 節點必須為 active 節點(p.prev==null&&p.next!=p)。first()源碼如下:
Node<E> first() {
    restartFromHead:
    for (;;)
        //從head開始往前找
        for (Node<E> h = head, p = h, q;;) {
            if ((q = p.prev) != null &&
                (q = (p = q).prev) != null)
                // Check for head updates every other hop.
                // If p == q, we are sure to follow head instead.
                //如果head被修改則返回新的head重新查找,否則繼續向前(prev)查找
                p = (h != (h = head)) ? h : q;
            else if (p == h
                     // It is possible that p is PREV_TERMINATOR,
                     // but if so, the CAS is guaranteed to fail.
                    //找到的節點不是head節點,CAS修改head
                     || casHead(h, p))
                return p;
            else
                continue restartFromHead;
        }
}
  1. 如果first.item==null(這里是允許的,具體見上面我們對 first/last 節點的介紹),則繼續調用succ方法尋找后繼節點。succ源碼如下:
/**返回指定節點的的后繼節點,如果指定節點的next指向自己,返回first節點*/
final Node<E> succ(Node<E> p) {
    // TODO: should we skip deleted nodes here?
    Node<E> q = p.next;
    return (p == q) ? first() : q;
}
  1. CAS 修改節點的 item 為 null(即 “邏輯刪除-logical deletion”),然后調用unlink(p)方法解除節點鏈接,最后返回 item。unlink(p)是移除節點的主方法,邏輯較為復雜,后面我們單獨分析。

unlink(Node<E> x)

/**
 * Unlinks non-null node x.
 */
void unlink(Node<E> x) {
    // assert x != null;
    // assert x.item == null;
    // assert x != PREV_TERMINATOR;
    // assert x != NEXT_TERMINATOR;

    final Node<E> prev = x.prev;
    final Node<E> next = x.next;
    if (prev == null) {//操作節點為first節點
        unlinkFirst(x, next);
    } else if (next == null) {//操作節點為last節點
        unlinkLast(x, prev);
    } else {// common case
        Node<E> activePred, activeSucc;
        boolean isFirst, isLast;
        int hops = 1;

        // Find active predecessor
        //從被操作節點的prev節點開始找到前繼活動節點
        for (Node<E> p = prev; ; ++hops) {          
            if (p.item != null) {
                activePred = p;
                isFirst = false;
                break;
            }
            Node<E> q = p.prev;
            if (q == null) {
                if (p.next == p)
                    return;//自鏈接節點
                activePred = p;
                isFirst = true;
                break;
            }
            else if (p == q)//自鏈接節點
                return;
            else
                p = q;
        }

        // Find active successor
        for (Node<E> p = next; ; ++hops) {          
            if (p.item != null) {
                activeSucc = p;
                isLast = false;
                break;
            }
            Node<E> q = p.next;
            if (q == null) {
                if (p.prev == p)
                    return;
                activeSucc = p;
                isLast = true;
                break;
            }
            else if (p == q)//自鏈接節點
                return;
            else
                p = q;
        }

        // TODO: better HOP heuristics
        //無節點跳躍并且操作節點有first或last節點時不更新鏈表
        if (hops < HOPS
            // always squeeze out interior deleted nodes
            && (isFirst | isLast))          
            return;

        // Squeeze out deleted nodes between activePred and
        // activeSucc, including x.
        //連接兩個活動節點
        skipDeletedSuccessors(activePred);            
        skipDeletedPredecessors(activeSucc);

        // Try to gc-unlink, if possible
        if ((isFirst | isLast) &&

            // Recheck expected state of predecessor and successor
            (activePred.next == activeSucc) &&
            (activeSucc.prev == activePred) &&
            (isFirst ? activePred.prev == null : activePred.item != null) &&
            (isLast  ? activeSucc.next == null : activeSucc.item != null)) {          

            updateHead(); // Ensure x is not reachable from head
            updateTail(); // Ensure x is not reachable from tail

            // Finally, actually gc-unlink
            x.lazySetPrev(isFirst ? prevTerminator() : x);
            x.lazySetNext(isLast  ? nextTerminator() : x);
        }
    }
}

說明unlink(Node<E> x)方法用于解除已彈出節點的鏈接,分三種情況:

  1. 首先說一下通常的情況(源碼中標注 common case 處),這種情況下,入列和出列非同端操作,即操作節點 x 非 first 和 last 節點, 就執行如下流程:
  • 首先找到給定節點 x 的活躍(active)前繼和后繼節點。然后修整它們之間的鏈接,讓它們指向對方(通過skipDeletedSuccessorsskipDeletedPredecessors方法),留下一個從活躍(active)節點不可達的 x 節點(即“unlinking”)。
  • 如果成功執行,或者 x 節點沒有 live 的前繼/后繼節點,再嘗試 gc 解除鏈接(gc-unlink),在設置 x 節點的 prev/next 指向它們自己或 TERMINATOR 之前(即“gc-unlink”),需要檢查 x 的前繼和后繼節點的狀態未被改變,并保證 x 節點從 head/tail 不可達(通過updateHead()updateTail()方法)。
  1. 如果操作節點為 first 節點(入列和出列都發生在 first 端),則調用unlinkFirst解除已刪除節點的鏈接,并鏈接 first 節點到下一個 active 節點(注意,在執行完此方法之后 first 節點是沒有改變的)。unlinkFirst源碼如下:
/**
 * Unlinks non-null first node.
 */
private void unlinkFirst(Node<E> first, Node<E> next) {
    // assert first != null;
    // assert next != null;
    // assert first.item == null;
    //從next節點開始向后尋找有效節點,o:已刪除節點(item為null)
    for (Node<E> o = null, p = next, q;;) {
        if (p.item != null || (q = p.next) == null) {
            //跳過已刪除節點,CAS替換first的next節點為一個active節點p
            if (o != null && p.prev != p && first.casNext(next, p)) {
                //更新p的prev節點
                skipDeletedPredecessors(p);
                if (first.prev == null &&
                    (p.next == null || p.item != null) &&
                    p.prev == first) {
                    //更新head節點,確保已刪除節點o從head不可達(unlinking)
                    updateHead(); // Ensure o is not reachable from head
                    //更新tail節點,確保已刪除節點o從tail不可達(unlinking)
                    updateTail(); // Ensure o is not reachable from tail

                    // Finally, actually gc-unlink
                    //使unlinking節點next指向自身
                    o.lazySetNext(o);
                    //設置移除節點的prev為PREV_TERMINATOR
                    o.lazySetPrev(prevTerminator());
                }
            }
            return;
        }
        else if (p == q)//自鏈接節點
            return;
        else {
            o = p;
            p = q;
        }
    }
}
  1. 如果操作節點為 last 節點(入列和出列都發生在 last 端),則調用unlinkLast解除已刪除節點的鏈接,并鏈接 last 節點到上一個 active 節點。unlinkLastunlinkFirst方法執行流程一致,只是操作的是 last 端,在此不多贅述。

小結

本章與 ConcurrentLinkedQueue 一篇中的非阻塞算法基本一致,只是為雙端操作定義了幾個可供操作的節點類型。
本章重點:理解 ConcurrentLinkedDeque 的非阻塞算法及節點刪除的三個狀態

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

推薦閱讀更多精彩內容