JUC源碼分析-集合篇(八):SynchronousQueue

SynchronousQueue 是一個同步阻塞隊列,它的每個插入操作都要等待其他線程相應的移除操作,反之亦然。SynchronousQueue 像是生產者和消費者的會合通道,它比較適合“切換”或“傳遞”這種場景:一個線程必須同步等待另外一個線程把相關信息/時間/任務傳遞給它。在之后的線程池源碼分析中我們也會見到它,所以理解本章對我們之后的線程池講解也會有很大幫助。

概述

SynchronousQueue(后面稱SQ)內部沒有容量,所以不能通過peek方法獲取頭部元素;也不能單獨插入元素,可以簡單理解為它的插入和移除是“一對”對稱的操作。為了兼容 Collection 的某些操作(例如contains),SQ 扮演了一個空集合的角色。
SQ 的一個典型應用場景是在線程池中,Executors.newCachedThreadPool() 就使用了它,這個構造使線程池根據需要(新任務到來時)創建新的線程,如果有空閑線程則會重復使用,線程空閑了60秒后會被回收。

SQ 為等待過程中的生產者或消費者線程提供可選的公平策略(默認非公平模式)。非公平模式通過棧(LIFO)實現,公平模式通過隊列(FIFO)實現。使用的數據結構是雙重隊列(Dual queue)雙重棧(Dual stack)(后面詳細講解)。FIFO通常用于支持更高的吞吐量,LIFO則支持更高的線程局部存儲(TLS)。

SQ 的阻塞算法可以歸結為以下幾點:

  • 使用了雙重隊列(Dual queue)雙重棧(Dual stack)存儲數據,隊列中的每個節點都可以是一個生產者或是消費者。(有關雙重隊列,請參考筆者另外一篇文章:JUC源碼分析-集合篇(四):LinkedTransferQueue,雙重棧與它原理一致)
  • 已取消節點引用指向自身,避免垃圾保留和內存損耗
  • 通過自旋和 LockSupport 的 park/unpark 實現阻塞,在高爭用環境下,自旋可以顯著提高吞吐量。

數據結構

SynchronousQueue 繼承關系

SQ 有三個內部類:

  1. Transferer:內部抽象類,只有一個transfer方法。SQ的puttake被統一為一個方法(就是這個transfer方法),因為在雙重隊列/棧數據結構中,puttake操作是對稱的,所以幾乎所有代碼都可以合并。
  2. TransferStack:繼承了內部抽象類 Transferer,實現了transfer方法,用于非公平模式下的隊列操作,數據按照LIFO的順序。內部通過單向鏈表 SNode 實現的雙重棧。
  3. TransferQueue:繼承了內部抽象類 Transferer,實現了transfer方法,用于公平模式下的隊列操作,數據按照FIFO的順序。內部通過單向鏈表 QNode 實現的雙重隊列。

SNode & QNode

SNode 是雙重棧的實現,內部除了基礎的鏈表指針和數據外,還維護了一個int型變量mode,它是實現雙重棧的關鍵字段,有三個取值:0代表消費者節點(take),1代表生產者節點(put),2 | mode(mode為當前操作者模式:put or take)代表節點已被匹配。此外還有一個match引用,用于匹配時標識匹配的節點,節點取消等待后match引用指向自身。

QNode 是雙重隊列的實現,通過isData實現雙重隊列。這個在JUC源碼分析-集合篇(四):LinkedTransferQueue 一篇中有講解,在此就不再贅述。


源碼解析

SQ 的 put/take 操作完全是由transfer方法實現,以put方法為例,

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}

可以看到調用了內部變量 transferer 的transfer的方法。其它例如offer、take、poll都與之類似,所以接下來我們主要針對transfer方法,來分析 SQ 公平模式和非公平模式的不同實現。

TransferStack.transfer()

E transfer(E e, boolean timed, long nanos) {
    
    SNode s = null; // constructed/reused as needed
    //根據所傳元素判斷為生產or消費
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
        SNode h = head;
        if (h == null || h.mode == mode) {  // empty or same-mode
            if (timed && nanos <= 0) {      // can't wait
                if (h != null && h.isCancelled())//head已經被匹配,修改head繼續循環
                    casHead(h, h.next);     // pop cancelled node
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {//構建新的節點s,放到棧頂
                //等待s節點被匹配,返回s.match節點m
                SNode m = awaitFulfill(s, timed, nanos);
                //s.match==s(等待被取消)
                if (m == s) {               // wait was cancelled
                    clean(s);//清除s節點
                    return null;
                }
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } else if (!isFulfilling(h.mode)) { //head節點還沒有被匹配,嘗試匹配 try to fulfill
            if (h.isCancelled())            // already cancelled
                //head已經被匹配,修改head繼續循環
                casHead(h, h.next);         // pop and retry

            //構建新節點,放到棧頂
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    //cas成功后s的match節點就是s.next,即m
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;

                    if (m.tryMatch(s)) {//嘗試匹配,喚醒m節點的線程
                        casHead(s, mn);     //彈出匹配成功的兩個節點,替換head pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   //匹配失敗,刪除m節點,重新循環 help unlink
                }
            }
        } else {                            //頭節點正在匹配 help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {//幫助頭節點匹配
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

說明:基本算法是循環嘗試以下三種行為之一:

  1. 如果棧為空或者已經包含了一個相同的 mode,此時分兩種情況:如果是非計時操作(offer、poll)或者已經超時,直接返回null;其他情況下就把當前節點壓進棧頂等待匹配(通過awaitFulfill方法),匹配成功后返回匹配節點的 item,如果節點取消等待就調用clean方法(后面單獨講解)清除取消等待的節點,并返回 null。

  2. 如果棧頂節點(head)還沒有被匹配(通過isFulfilling方法判斷),則把當前節點壓入棧頂,并嘗試與head節點進行匹配,匹配成功后從棧中彈出這兩個節點,并返回匹配節點的數據。isFulfilling源碼如下:

/** Returns true if m has fulfilling bit set. */
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }
  1. 如果棧頂節點(head)已經持有另外一個數據節點,說明棧頂節點正在匹配,則幫助此節點進行匹配操作,然后繼續從第一步開始循環。

TransferStack. awaitFulfill()

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    //計算截止時間
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    //計算自旋次數
    int spins = (shouldSpin(s) ?
                 (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        if (w.isInterrupted())//當前線程被中斷
            //取消對給定節點s的匹配節點的等待
            s.tryCancel();
        SNode m = s.match;//獲取給定節點s的match節點
        if (m != null)//已經匹配到,返回匹配節點
            return m;
        if (timed) {
            //超時處理
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel();//超時,取消s節點的匹配,match指向自身
                continue;
            }
        }
        if (spins > 0)
            //spins-1
            spins = shouldSpin(s) ? (spins-1) : 0;
        else if (s.waiter == null)
            //設置給定節點s的waiter為當前線程
            s.waiter = w; // establish waiter so can park next iter
        else if (!timed)//沒有設定超時,直接阻塞
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)//阻塞指定超時時間
            LockSupport.parkNanos(this, nanos);
    }
}

說明:如果當前操作是一個不計時操作,或者是一個還未到超時時間的操作,就構建新的節點壓入棧頂。然后調用此方法自旋/阻塞等待給定節點s被匹配。
當調用此方法時,所傳參數節點s一定是在棧頂,節點真正阻塞前會先自旋,以防生產者和消費者到達的時間點非常接近時也被 park

當節點/線程需要阻塞時,首先設置waiter字段為當前線程,然后在真正阻塞之前重新檢查一下waiter的狀態,因為在線程競爭中,需要確認waiter沒有被其他線程占用。
從主循環返回的檢查順序可以反映出中斷優先于正常返回。除了不計時操作(poll/offer)不會檢查中斷,而是直接在transfer方法中入棧等待匹配。


TransferQueue.transfer()

E transfer(E e, boolean timed, long nanos) {
    QNode s = null; // constructed/reused as needed
    boolean isData = (e != null);//判斷put or take

    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin

        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            if (t != tail)                  // inconsistent read
                continue;
            if (tn != null) {               //尾節點滯后,更新尾節點 lagging tail
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)        // can't wait
                return null;
            //為當前操作構造新節點,并放到隊尾
            if (s == null)
                s = new QNode(e, isData);
            if (!t.casNext(null, s))        // failed to link in
                continue;

            //推進tail
            advanceTail(t, s);              // swing tail and wait
            //等待匹配,并返回匹配節點的item,如果取消等待則返回該節點s
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s); //等待被取消,清除s節點
                return null;
            }

            if (!s.isOffList()) {           // s節點尚未出列 not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;//item指向自身
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

            //take
        } else {                            // complementary-mode
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   //m.item=m, m cancelled
                !m.casItem(x, e)) {         // 匹配,CAS修改item為給定元素e lost CAS
                advanceHead(h, m);          // 推進head,繼續向后查找 dequeue and retry
                continue;
            }

            advanceHead(h, m);              //匹配成功,head出列 successfully fulfilled
            LockSupport.unpark(m.waiter);   //喚醒被匹配節點m的線程
            return (x != null) ? (E)x : e;
        }
    }
}

說明:基本算法是循環嘗試以下兩個動作中的其中一個:

  1. 若隊列為空或者隊列中的尾節點(tail)和自己的模式相同,則把當前節點添加到隊列尾,調用awaitFulfill等待節點被匹配。匹配成功后返回匹配節點的 item,如果等待節點被中斷或等待超時返回null。在此期間會不斷檢查tail節點,如果tail節點被其他線程修改,則向后推進tail繼續循環嘗試。
    注:TransferQueue 的 awaitFulfill方法與 TransferStack.awaitFulfill算法一致,后面就不再講解了。

  2. 如果當前操作模式與尾節點(tail)不同,說明可以進行匹配,則從隊列頭節點head開始向后查找一個互補節點進行匹配,嘗試通過CAS修改互補節點的item字段為給定元素e,匹配成功后向后推進head,并喚醒被匹配節點的waiter線程,最后返回匹配節點的item


棧/隊列節點清除的對比(clean方法)

在隊列和棧中進行清理的方式不同:
對于隊列來說,如果節點被取消,我們幾乎總是可以以 O1 的時間復雜度移除節點。但是如果節點在隊尾,它必須等待后面節點的取消。
對于棧來說,我們可能需要 O(n) 的時間復雜度去遍歷整個棧,然后確定節點可被移除,但這可以與訪問棧的其他線程并行運行。

下面我們來看一下 TransferStack 和 TransferQueue 對節點清除方法的優化:

TransferStack.clean(SNode s)
void clean(SNode s) {
    s.item = null;   // forget item
    s.waiter = null; // forget thread

    SNode past = s.next;
    if (past != null && past.isCancelled())
        past = past.next;

    // Absorb cancelled nodes at head
    //找到有效head
    SNode p;
    while ((p = head) != null && p != past && p.isCancelled())
        casHead(p, p.next);

    // Unsplice embedded nodes
    //移除head到past中已取消節點的鏈接
    while (p != null && p != past) {
        SNode n = p.next;
        if (n != null && n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}

說明:在最壞的情況下可能需要遍歷整個棧來解除給定節點s的鏈接(例如給定節點在棧底)。在并發情況下,如果有其他線程已經移除給定節點s,當前線程可能無法看到,但是我們可以使用這樣一種算法:
使用s.next作為past節點,如果past節點已經取消,則使用past.next節點,然后依次解除從headpast中已取消節點的鏈接。在這里不會做更深的檢查,因為為了找到失效節點而進行兩次遍歷是不值得的。

TransferQueue.clean(QNode pred, QNode s)
void clean(QNode pred, QNode s) {
    s.waiter = null; // forget thread
    
    while (pred.next == s) { // Return early if already unlinked
        QNode h = head;
        QNode hn = h.next;   // Absorb cancelled first node as head
        //找到有效head節點
        if (hn != null && hn.isCancelled()) {
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        if (t == h)//隊列為空,直接返回
            return;
        QNode tn = t.next;
        if (t != tail)//tail節點被其他線程修改,重新循環
            continue;
        //找到tail節點
        if (tn != null) {
            advanceTail(t, tn);
            continue;
        }
        if (s != t) {        // If not tail, try to unsplice
            QNode sn = s.next;
            if (sn == s || pred.casNext(s, sn))//cas解除s的鏈接
                return;
        }
        //s是隊列尾節點,此時無法刪除s,只能去清除cleanMe節點
        QNode dp = cleanMe;
        if (dp != null) {    // Try unlinking previous cancelled node
            QNode d = dp.next;
            QNode dn;
            if (d == null ||               // d is gone or
                d == dp ||                 // d is off list or
                !d.isCancelled() ||        // d not cancelled or
                (d != t &&                 // d not tail and
                 (dn = d.next) != null &&  //   has successor
                 dn != d &&                //   that is on list
                 dp.casNext(d, dn)))       // d unspliced
                casCleanMe(dp, null);
            if (dp == pred)
                return;      // s is already saved node
        } else if (casCleanMe(null, pred))//原cleanMe為空,標記pred為cleanMe,延遲清除s節點
            return;          // Postpone cleaning s
    }
}

說明:方法參數中s為已經取消的節點,preds的前繼節點。
任何時候在隊列中都存在一個不能刪除的節點,也就是最后被插入的那個節點(tail節點)。為了滿足這一點,在 TransferQueue 中維護了一個cleanMe節點引用。當給定s節點為tail節點時,首先刪除cleanMe節點引用;然后保存s的前繼節點作為cleanMe節點,在下次清除操作時再清除節點。這樣保證了在s節點和cleanMe節點中至少有一個是可以刪除的。


小結

本章重點:理解 SynchronousQueue 中雙重棧和雙重隊列的實現;理解 SynchronousQueue 的阻塞算法

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

推薦閱讀更多精彩內容