JUC源碼分析-集合篇(六):LinkedTransferQueue

LinkedTransferQueue 是單向鏈表結構的無界阻塞隊列, 從JDK1.7開始加入到J.U.C的行列中。通過 CAS 和 LockSupport 實現線程安全,元素操作按照 FIFO (first-in-first-out 先入先出) 的順序。內存一致性遵循對LinkedTransferQueue的插入操作先行發生于(happen-before)訪問或移除操作。相對于其他傳統 Queue,LinkedTransferQueue 有它獨特的性質,本章將對其進行詳細的講解。

概述

LinkedTransferQueue(后稱LTQ) 采用一種預占模式。意思就是消費者線程取元素時,如果隊列為空,那就生成一個節點(節點元素為null)入隊,然后消費者線程被等待在這個節點上,后面生產者線程入隊時發現有一個元素為null的節點,生產者線程就不入隊了,直接就將元素填充到該節點,并喚醒該節點等待的線程,被喚醒的消費者線程取走元素,從調用的方法返回。我們稱這種節點操作為“匹配”方式。

LTQ的算法實現可以總結為以下幾點:

  • 雙重隊列
    和典型的單向鏈表結構不同,LTQ 的 Node 存儲了一個isData的 boolean 型字段,也就是說它的節點可以代表一個數據或者是一個請求,稱為雙重隊列(Dual Queue)。上面說過,在消費者獲取元素時,如果隊列為空,當前消費者就會作為一個“元素為null”的節點被放入隊列中等待,所以 LTQ中 的節點存儲了生產者節點(item不為null)和消費者節點(item為null),這兩種節點就是通過isData來區分的。

  • 松弛度
    為了節省 CAS 操作的開銷,LTQ 引入了“松弛度”的概念:在節點被匹配(被刪除)之后,不會立即更新head/tail,而是當 head/tail 節點和最近一個未匹配的節點之間的距離超過一個“松弛閥值”之后才會更新(在 LTQ 中,這個值為 2)。這個“松弛閥值”一般為1-3,如果太大會降低緩存命中率,并且會增加遍歷鏈的長度;太小會增加 CAS 的開銷。

  • 節點自鏈接
    已匹配節點的 next 引用會指向自身。
    如果GC延遲回收,已刪除節點鏈會積累的很長,此時垃圾收集會耗費高昂的代價,并且所有剛匹配的節點也不會被回收。為了避免這種情況,我們在 CAS 向后推進 head 時,會把已匹配的 head 的"next"引用指向自身(即“自鏈接節點”),這樣就限制了連接已刪除節點的長度(我們也采取類似的方法,清除在其他節點字段中可能的垃圾保留值)。如果在遍歷時遇到一個自鏈接節點,那就表明當前線程已經滯后于另外一個更新 head 的線程,此時就需要重新獲取 head 來遍歷。

所以,在 LTQ 中,數據在某個線程的“某一時刻”可能存在下面這種形式:

LinkedTransferQueue 數據形式

unmatched node:未被匹配的節點。可能是一個生產者節點(item不為null),也可能是一個消費者節點(item為null)。
matched node:已經被匹配的節點。可能是一個生產者節點(item不為null)的數據已經被一個消費者拿走;也可能是一個消費者節點(item為null)已經被一個生產者填充上數據。


數據結構

LinkedTransferQueue 繼承關系

LTQ 繼承自AbstractQueue,支持傳統Queue的所有操作;實現了 TransferQueue 接口,并且是 TransferQueue 的唯一實現,TransferQueue 定義了一種“預占模式”,允許消費者在節點上等待,直到生產者把元素放入節點。


核心參數

//隊列頭節點,第一次入列之前為空
transient volatile Node head;

//隊列尾節點,第一次添加節點之前為空
private transient volatile Node tail;

//累計到一定次數再清除無效node
private transient volatile int sweepVotes;

//當一個節點是隊列中的第一個waiter時,在多處理器上進行自旋的次數(隨機穿插調用thread.yield)
private static final int FRONT_SPINS   = 1 << 7;

// 當前繼節點正在處理,當前節點在阻塞之前的自旋次數,也為FRONT_SPINS
// 的位變化充當增量,也可在自旋時作為yield的平均頻率
private static final int CHAINED_SPINS = FRONT_SPINS >>> 1;

//sweepVotes的閥值
static final int SWEEP_THRESHOLD = 32;
/*
 * Possible values for "how" argument in xfer method.
 * xfer方法類型
 */
private static final int NOW   = 0; // for untimed poll, tryTransfer
private static final int ASYNC = 1; // for offer, put, add
private static final int SYNC  = 2; // for transfer, take
private static final int TIMED = 3; // for timed poll, tryTransfer

這里我們重點說一下sweepVotes這個屬性,其他的都很簡單,就不一一介紹了。

上面我們提到,head/tail 節點并不是及時更新的,在并發操作時鏈表內部可能存在已匹配節點,此時就需要一個閥值來決定何時清除已匹配的內部節點鏈,這就是sweepVotesSWEEP_THRESHOLD的作用。

我們通過節點自鏈接的方式來減少垃圾滯留,同樣也會解除內部已移除節點的鏈接。在匹配超時、線程中斷或調用remove時,這也些節點也會被清除(解除鏈接)。例如,在某一時刻有一個節點 s 已經被移除,我們可以通過 CAS 修改 s 的前繼節點的 next 引用的方式來解除 s 的鏈接。 但是有兩種情況并不能保證節點 s 被解除鏈接:
1. 如果 s 節點是一個 next 為 null 的節點(trailing node),但是它被作為入列時的目標節點,所以只有在其他節點入列之后才能移除它
2. 通過給定 s 的前繼節點,不一定會移除 s 節點:因為前繼節點有可能已經被解除鏈接,這種情況下前繼節點的前繼節點有可能指向了s。

所以,通過這兩點,說明在 s 節點或它的前繼節點已經出列時,并不是必須要移除它們。對于這些情況,我們記錄了一個解除節點鏈接失敗的值-sweepVotes,并且為其定義了一個閥值-SWEEP_THRESHOLD,當解除鏈接失敗次數超過這個閥值時就會對隊列進行一次“大掃除”(通過sweep()方法),解除所有已取消的節點鏈接。

xfer方法類型
在 LTQ 中,所有的入隊/出隊操作都是通過xfer方法來控制,并且通過一個類型區分offer, put, poll, take, transfer,這樣做大大簡化了代碼。來看一下xfer的方法類型:
NOW:不等待,直接返回匹配結果。用在poll, tryTransfer中。
ASYNC:異步操作,直接把元素添加到隊列尾,不等待匹配。用在offer, put, add中。
SYNC:等待元素被消費者接收。用在transfer, take中。
TIMED:附帶超時時間的NOW,等待指定時間后返回匹配結果。用在附帶超時時間的poll, tryTransfer中。


源碼解析

由于 LTQ 的入列/出列方法都是由xfer來實現,所以我們這里只對xfer進行解析。

xfer(E e, boolean haveData, int how, long nanos)

/**
 * Implements all queuing methods. See above for explanation.
 *
 * @param e the item or null for take
 * @param haveData true if this is a put, else a take
 * @param how NOW, ASYNC, SYNC, or TIMED
 * @param nanos timeout in nanosecs, used only if mode is TIMED
 * @return an item if matched, else e
 * @throws NullPointerException if haveData mode but e is null
 */
private E xfer(E e, boolean haveData, int how, long nanos) {
    if (haveData && (e == null))
        throw new NullPointerException();
    Node s = null;                        // the node to append, if needed

    retry:
    for (;;) {                            // restart on append race
        //從head開始向后匹配
        for (Node h = head, p = h; p != null;) { // find & match first node
            boolean isData = p.isData;
            Object item = p.item;
            if (item != p && (item != null) == isData) { // 找到有效節點,進入匹配
                if (isData == haveData)   //節點與此次操作模式一致,無法匹配 can't match
                    break;
                if (p.casItem(item, e)) { // 匹配成功,cas修改為指定元素 match
                    for (Node q = p; q != h;) {
                        Node n = q.next;  // update by 2 unless singleton
                        if (head == h && casHead(h, n == null ? q : n)) {//更新head為匹配節點的next節點
                            h.forgetNext();//舊head節點指向自身等待回收
                            break;
                        }                 // cas失敗,重新獲取head  advance and retry
                        if ((h = head)   == null ||
                                (q = h.next) == null || !q.isMatched())//如果head的next節點未被匹配,跳出循環,不更新head,也就是松弛度<2
                            break;        // unless slack < 2
                    }
                    LockSupport.unpark(p.waiter);//喚醒在節點上等待的線程
                    return LinkedTransferQueue.<E>cast(item);
                }
            }
            //匹配失敗,繼續向后查找節點
            Node n = p.next;
            p = (p != n) ? n : (h = head); // Use head if p offlist
        }

        //未找到匹配節點,把當前節點加入到隊列尾
        if (how != NOW) {                 // No matches available
            if (s == null)
                s = new Node(e, haveData);
            //將新節點s添加到隊列尾并返回s的前繼節點
            Node pred = tryAppend(s, haveData);
            if (pred == null)
                continue retry;           //與其他不同模式線程競爭失敗重新循環 lost race vs opposite mode
            if (how != ASYNC)//同步操作,等待匹配
                return awaitMatch(s, pred, e, (how == TIMED), nanos);
        }
        return e; // not waiting
    }
}

說明xfer的基本流程如下:

  1. 從head開始向后匹配,找到一個節點模式跟本次操作的模式不同的未匹配的節點(生產或消費)進行匹配;
  2. 匹配節點成功 CAS 修改匹配節點的 item 為給定元素 e;
  3. 如果此時所匹配節點向后移動,則 CAS 更新 head 節點為匹配節點的 next 節點,舊 head 節點鏈接指向自身等待被回收(forgetNext()方法);如果CAS 失敗,并且松弛度大于等于2,就需要重新獲取 head 重試。
  4. 匹配成功,喚醒匹配節點 p 的等待線程 waiter,返回匹配的 item。
  5. 如果在上述操作中沒有找到匹配節點,則根據參數how做不同的處理:
    NOW:立即返回。
    SYNC:通過tryAppend方法插入一個新的節點 s(item=e,isData = haveData)到隊列尾,然后自旋或阻塞當前線程直到節點被匹配或者取消返回。
    ASYNC:通過tryAppend方法插入一個新的節點 s(item=e,isData = haveData)到隊列尾,異步直接返回。
    TIMED:通過tryAppend方法插入一個新的節點 s(item=e,isData = haveData)到隊列尾,然后自旋或阻塞當前線程直到節點被匹配或者取消或等待超時返回。

tryAppend(Node s, boolean haveData)

/**
 * Tries to append node s as tail.
 * 嘗試添加給定節點s作為尾節點
 *
 * @param s the node to append
 * @param haveData true if appending in data mode
 * @return null on failure due to losing race with append in
 * different mode, else s's predecessor, or s itself if no
 * predecessor
 */
private Node tryAppend(Node s, boolean haveData) {
    for (Node t = tail, p = t;;) {        // move p to last node and append
        Node n, u;                        // temps for reads of next & tail
        if (p == null && (p = head) == null) {//head和tail都為null
            if (casHead(null, s))//修改head為新節點s
                return s;                 // initialize
        }
        else if (p.cannotPrecede(haveData))
            return null;                  // lost race vs opposite mode
        else if ((n = p.next) != null)    // not last; keep traversing
            p = p != t && t != (u = tail) ? (t = u) : // stale tail
                (p != n) ? n : null;      // restart if off list
        else if (!p.casNext(null, s))
            p = p.next;                   // re-read on CAS failure
        else {
            if (p != t) {                 // update if slack now >= 2
                while ((tail != t || !casTail(t, s)) &&
                       (t = tail)   != null &&
                       (s = t.next) != null && // advance and retry
                       (s = s.next) != null && s != t);
            }
            return p;
        }
    }
}

說明:添加給定節點 s 到隊列尾并返回 s 的前繼節點,失敗時(與其他不同模式線程競爭失敗)返回null,沒有前繼節點返回自身。

awaitMatch(Node s, Node pred, E e, boolean timed, long nanos)

/**
 * Spins/yields/blocks until node s is matched or caller gives up.
 * 自旋/讓步/阻塞,直到給定節點s匹配到或放棄匹配
 *
 * @param s the waiting node
 * @param pred the predecessor of s, or s itself if it has no
 * predecessor, or null if unknown (the null case does not occur
 * in any current calls but may in possible future extensions)
 * @param e the comparison value for checking match
 * @param timed if true, wait only until timeout elapses
 * @param nanos timeout in nanosecs, used only if timed is true
 * @return matched item, or e if unmatched on interrupt or timeout
 */
private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    //在首個item和取消檢查后初始
    int spins = -1; // initialized after first item and cancel checks
    ThreadLocalRandom randomYields = null; // bound if needed

    for (;;) {
        Object item = s.item;
        if (item != e) {                  //matched
            // assert item != s;
            s.forgetContents();           // avoid garbage
            return LinkedTransferQueue.<E>cast(item);
        }
        if ((w.isInterrupted() || (timed && nanos <= 0)) &&
                s.casItem(e, s)) {        //取消匹配,item指向自身 cancel
            unsplice(pred, s);//解除s節點和前繼節點的鏈接
            return e;
        }

        if (spins < 0) {                  // establish spins at/near front
            if ((spins = spinsFor(pred, s.isData)) > 0)
                randomYields = ThreadLocalRandom.current();
        }
        else if (spins > 0) {             // spin
            --spins;
            if (randomYields.nextInt(CHAINED_SPINS) == 0)
                Thread.yield();           //不定期讓步,給其他線程執行機會 occasionally yield
        }
        else if (s.waiter == null) {
            s.waiter = w;                 // request unpark then recheck
        }
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos > 0L)
                LockSupport.parkNanos(this, nanos);
        }
        else {
            LockSupport.park(this);
        }
    }
}

說明:當前操作為同步操作時,會調用awaitMatch方法阻塞等待匹配,成功返回匹配節點 item,失敗返回給定參數e(s.item)。在等待期間如果線程被中斷或等待超時,則取消匹配,并調用unsplice方法解除節點s和其前繼節點的鏈接。

/**
 * Unsplices (now or later) the given deleted/cancelled node with
 * the given predecessor.
 *
 * 解除給定已經被刪除/取消節點和前繼節點的鏈接(可能延遲解除)
 * @param pred a node that was at one time known to be the
 * predecessor of s, or null or s itself if s is/was at head
 * @param s the node to be unspliced
 */
final void unsplice(Node pred, Node s) {
    s.forgetContents(); // forget unneeded fields
   
    if (pred != null && pred != s && pred.next == s) {
        Node n = s.next;
        if (n == null ||
            (n != s && pred.casNext(s, n) && pred.isMatched())) {//解除s節點的鏈接
            for (;;) {               // check if at, or could be, head
                Node h = head;
                if (h == pred || h == s || h == null)
                    return;          // at head or list empty
                if (!h.isMatched())
                    break;
                Node hn = h.next;
                if (hn == null)
                    return;          // now empty
                if (hn != h && casHead(h, hn))//更新head
                    h.forgetNext();  // advance head
            }
            if (pred.next != pred && s.next != s) { // recheck if offlist
                for (;;) {           // sweep now if enough votes
                    int v = sweepVotes;
                    if (v < SWEEP_THRESHOLD) {
                        if (casSweepVotes(v, v + 1))
                            break;
                    }
                    else if (casSweepVotes(v, 0)) {//達到閥值,進行"大掃除",清除隊列中的無效節點
                        sweep();
                        break;
                    }
                }
            }
        }
    }
}

說明:首先把給定節點s的next引用指向自身,如果s的前繼節點pred還是指向spred.next == s),嘗試解除s的鏈接,把pred的 next 引用指向s的 next 節點。如果s不能被解除(由于它是尾節點或者pred可能被解除鏈接,并且preds都不是head節點或已經出列),則添加到sweepVotessweepVotes累計到閥值SWEEP_THRESHOLD之后就調用sweep()對隊列進行一次“大掃除”,清除隊列中所有的無效節點。sweep()源碼如下:

/**
 * Unlinks matched (typically cancelled) nodes encountered in a
 * traversal from head.
 * 解除(通常是取消)從頭部遍歷時遇到的已經被匹配的節點的鏈接
 */
private void sweep() {
    for (Node p = head, s, n; p != null && (s = p.next) != null; ) {
        if (!s.isMatched())
            // Unmatched nodes are never self-linked
            p = s;
        else if ((n = s.next) == null) // trailing node is pinned
            break;
        else if (s == n)    // stale
            // No need to also check for p == s, since that implies s == n
            p = head;
        else
            p.casNext(s, n);
    }
}

小結

本章重點:理解 LinkedTransferQueue 的特性:雙重隊列、松弛度、節點的移除操作。
在 ConcurrentLinkedQueue 、 ConcurrentLinkeDeque 以及 SynchronousQueue 中都用到了 LinkedTransferQueue 的某些特性,如果同學們對它們感興趣,理解本章對之后的源碼解析會有很大的幫助。

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

推薦閱讀更多精彩內容