【Java Collection】子類 SynchronousQueue 圖解剖析(五)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、前言

  1. 最好先閱讀【Java Collection】Queue 剖析(四)
  2. 必須先閱讀【SynchronousQueue 簡述 】

二、架構

2.1 UML 圖

在這裏插入圖片描述

2.2 TransferStack流程圖

2.2.1 節點匹配流程圖
2.2.2 transfer 流程圖
2.2.3 awaitFulfill 流程圖

2.3 TransferQueue流程圖

2.3.1 節點匹配流程圖
2.3.2 TransferQueue#transfer 流程圖
2.3.3 TransferQueue#clean 流程圖

2.4 特性

  • 生產1次,消費1次,再生產,再消費,否則堵塞。
  • SynchronousQueue 既繼承了 AbstractQueue抽象類又實現了 BlockingQueue,跟 LinkedBlockingQueue 一樣
public class SynchronousQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -3223113410248163686L;
  • Java 6的SynchronousQueue的實現採用了一種性能更好的無鎖算法 — 擴展的“Dual stack and Dual queue”算法。

2.5 SynchronousQueue 與 AbstractQueuedSynchronizer 的關係

  1. AbstractQueuedSynchronizer 內部其實是加了鎖 Reentrantlock,SynchronousQueue 內部不加鎖,先 spin 自旋,不行就去 LockSupport.park(this); 阻塞

=== 點擊查看top目錄 ===

三、SynchronousQueue 剖析

3.1 構造方法

	private transient volatile Transferer<E> transferer;

    public SynchronousQueue() {
        this(false);
    }

    public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }
  • 默認非公平鎖
  • 競爭機制支持公平和非公平兩種:非公平競爭模式使用的數據結構是後進先出棧(Lifo Stack);公平競爭模式則使用先進先出隊列(Fifo Queue),性能上兩者是相當的,一般情況下,Fifo通常可以支持更大的吞吐量,但Lifo可以更大程度的保持線程的本地化。

=== 點擊查看top目錄 ===

3.2 take 方法 (取元素)

public E take() throws InterruptedException {
        E e = transferer.transfer(null, false, 0);
        if (e != null)
            return e;
        Thread.interrupted();
        throw new InterruptedException();
    }

=== 點擊查看top目錄 ===

3.3 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();
        }
    }

=== 點擊查看top目錄 ===

3.4 內部靜態抽象類 Transferer

	abstract static class Transferer<E> {
        abstract E transfer(E e, boolean timed, long nanos);
    }

=== 點擊查看top目錄 ===

3.5 take 與 put 內部區別

兩者底層都是調用了 transferer.transfer(E e, boolean timed, long nanos); 方法,區別在於:

  1. 第一個參數E e,,若是元素 e,表示是要入隊列 put,若是 null,表示是要取數據 take。
  2. 第二個參數 boolean timed,即是否會超時,若是 true,表明是在一定時間內嘗試,若是 false,則表示一直嘗試,永不退出。
  3. 第三個參數long nanos ,表示嘗試多久,若第二個參數是 false,這個參數就是 0.

=== 點擊查看top目錄 ===

3.6 變量 cleanMe

transient volatile QNode cleanMe;
  • 節點存在的作用是:標記它的下個節點要刪除
  • 何時使用:當你要刪除節點 node, 若節點 node 是隊列的末尾, 則開始用這個節點
  • 爲什麼要這樣呢?
    因爲刪除一個節點直接 A.CASNext(B, B.next) 就可以,但是當節點 B 是整個隊列中的末尾元素時, 一個線程刪除節點B, 一個線程在節點B之後插入節點這樣操作容易致使插入的節點丟失, 這個cleanMe很像ConcurrentSkipListMap 中的刪除添加的 marker 節點。

四、 TransferStack 棧(隊列出入非公平,先進後出,常用)

  • 根據 debug 學習法,咱們直接看TransferStack 的 transfer 方法

4.1 SNode 節點

static final class SNode {
 // next指向棧中下一個元素
 volatile SNode next;        // next node in stack
 // 和當前節點匹配的節點
 volatile SNode match;       // the node matched to this
 // 等待線程
 volatile Thread waiter;     // to control park/unpark
 // 節點內容
 Object item;                // data; or null for REQUESTs
 // 節點類型
 int mode;
  • 匹配流程圖
    在這裏插入圖片描述

=== 點擊查看top目錄 ===

4.2 transfer 方法

transfer 流程圖

在這裏插入圖片描述

E transfer(E e, boolean timed, long nanos) {

	// 既然是 queue 隊列,內部肯定都是用 Node 包裝
    SNode s = null; 
    /*
    	mode 模式(操作):
    	 若是 null,那麼是 take 操作,也就是 REQUES
    	 若是元素 e,那麼是 put 操作,也就是 DATA
	 */
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
    	// 這個 h = head, 一開始肯定是 null
        SNode h = head;
        // 要麼 head 是 null,要麼隊列中 mode一樣,全都是 take 或者全都是 put,進入下面的 if 
        if (h == null || h.mode == mode) {  // empty or same-mode
        	/*
        	 timed = true :表示如果有規定超時 timeout 時間。
        	 nanos <= 0 : 表示到點了,該回家了
        	 */
            if (timed && nanos <= 0) {      // can't wait
                if (h != null && h.isCancelled())
                	// 節點已經timeout 取消了,pop出 head節點
                    casHead(h, h.next);     // pop cancelled node
                else
                    return null; 
                /*
                 	put 的時候,把 element 塞到 head 的位置(往頭部插入)
                 	cas 設置頭節點 head 是 s (s節點包裝了入queue 的 e)
		
                 */
            } else if (casHead(h, s = snode(s, e, h, mode))) {
            	/*
            	 	等待結束,也就是put的時候,阻塞在這裏等被 take,
					也就是 take 的時候,阻塞在這裏等被 put.
            	 	這個方法重點講。
            	 	// 通過awaitFulfill方法自旋阻塞找到匹配操作的節點
            	 */
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled,說明已經超時退出了
                    clean(s); // 清理 s 節點
                    return null; // 返回 null
                }
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
            /*
             	還未完成任務 fulfill 
             	能跑到這個if,說明head 隊列是 take,突然來了個 put ,或者head 隊列是 put,突然來了個 take
             */
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
                /* 
                	這個地方如果是取 take 的話,那麼 s節點內是 null 
                	首先把 take 的 Node 插入 head,然後此時 head 後面假如有 2個 put 的Node阻塞着。
                */
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                	// 接上面,這個地方 m = s.next ,也就是阻塞的 put Node
                    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
                    }
                    // mn 是下一個 Node
                    SNode mn = m.next;
                    // 嘗試匹配,也就是一個 put 匹配一個 take,匹配上的話,倆倆牽走。匹配成功進入
                    if (m.tryMatch(s)) {
                    	// 把 s 和 m 都拿掉,因爲他們牽走走了。所以把 head 設置成 mn,也就是第3個
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                    	/*
                    		 如果匹配不成功,那麼拿掉m節點。繼續進入 for
                    		 匹配不成功是因爲在那一瞬間被別人搶走了
                		*/
                        s.casNext(m, mn);   // 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
            }
        }
    }
}

=== 點擊查看top目錄 ===

4.3 casHead 方法

  • java.util.concurrent.SynchronousQueue.TransferStack#casHead 方法
boolean casHead(SNode h, SNode nh) {
            return h == head &&
                UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
        }
  • sun.misc.Unsafe#compareAndSwapObject 方法
public final native boolean compareAndSwapObject(Object o, long offset,
                                                     Object expected,
                                                     Object x);
  • 這個簡單,只是利用底層的 CAS,把head節點給換了

=== 點擊查看top目錄 ===

4.4 isCancelled 方法

  • java.util.concurrent.SynchronousQueue.TransferStack.SNode#isCancelled 方法
  • 取消操作(被外部中斷或者超時):match == this;
 boolean isCancelled() {
    return match == this;
}

=== 點擊查看top目錄 ===

4.5 isFulfilling 方法

/** Node represents an unfulfilled consumer */
static final int REQUEST    = 0;
/**Node represents an unfulfilled producer */
static final int DATA       = 1;
/** Node is fulfilling another unfulfilled DATA or REQUEST */
static final int FULFILLING = 2;
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }
  • 這個方法只有當m = FULFILLING 的時候,才返回 true

=== 點擊查看top目錄 ===

4.6 tryMatch 方法

  • 上游
    在這裏插入圖片描述

  • 嘗試s節點與當前節點進行匹配,成功則喚醒等待線程繼續執行

boolean tryMatch(SNode s) {
      if (match == null &&
          UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
          Thread w = waiter;
          if (w != null) {    // waiters need at most one unpark
         	 // 喚醒等待線程同時將waiter置空
              waiter = null;
              LockSupport.unpark(w);
          }
          return true;
      }
      // 判斷當前節點是否已與s進行匹配
      return match == s;
  }

=== 點擊查看top目錄 ===

4.7 awaitFulfill 方法

  • java.util.concurrent.SynchronousQueue.TransferStack#awaitFulfill 方法
  • 未設置操作時間同時未被外部線程中斷則需阻塞等待匹配節點喚醒當前阻塞的線程
awaitFulfill 流程圖

在這裏插入圖片描述

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
        // 獲取超時時間點
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        // 當前線程
        Thread w = Thread.currentThread();
        // shouldSpin判斷是否需要進行自旋
        int spins = (shouldSpin(s) ?
                     (timed ? maxTimedSpins : maxUntimedSpins) : 0);            
        for (;;) {
            // 判斷當前線程是否中斷,外部中斷操作,相當於取消本次操作
            if (w.isInterrupted())
                // 嘗試將s節點的match設置爲s自己,這樣判斷的時候就知道這個節點是被取消的
                s.tryCancel();
            SNode m = s.match;
            // match非空則表示當前節點已經被匹配match匹配上
            if (m != null)
                return m;
            // 超時配置處理
            if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    s.tryCancel();
                    continue;
                }
            }
            // 自旋spins
            if (spins > 0)
                spins = shouldSpin(s) ? (spins-1) : 0;
            // 設置等待線程
            else if (s.waiter == null)
                s.waiter = w; 
            // 未設置超時,直接阻塞
            else if (!timed)
                LockSupport.park(this);
            // 設置超時時間阻塞
            // 阻塞時間是否超過1000nm納秒?如果是的話,一直自旋臺耗CPU了,還是老老實實去睡覺吧。
            else if (nanos > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanos);
        }
    }

=== 點擊查看top目錄 ===

4.8 shouldSpin 方法

  • java.util.concurrent.SynchronousQueue.TransferStack#shouldSpin 方法
  • 判斷是否需要自旋操作,3種情況需要自旋
  1. 棧頂節點等於s節點
  2. 棧頂節點爲空
  3. 棧頂節點爲已和其他節點匹配的節點(mode = FULFILLING|mode)
boolean shouldSpin(SNode s) {
      SNode h = head;
      return (h == s || h == null || isFulfilling(h.mode));
  }

=== 點擊查看top目錄 ===

4.9 maxTimedSpins 變量與 maxUntimedSpins 變量

	static final int NCPUS = Runtime.getRuntime().availableProcessors();
	static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
    static final int maxUntimedSpins = maxTimedSpins * 16;
  • maxTimedSpins 變量
  1. 如果cpu個數是1,那麼自旋spin 0次,直接阻塞
  2. 如果cpu個數大於1 ,那麼自旋32次。
  • maxUntimedSpins 變量( = maxTimedSpins * 16 = 512)
  • 解析下這個語句(位於 awaitFulfill 方法):
int spins = (shouldSpin(s) ?
            (timed ? maxTimedSpins : maxUntimedSpins) : 0);
  • 計算自旋次數
  1. 如果不需要自旋,那麼直接 spins = 0
  2. 如果需要自旋,並且會超時,那麼 spins = maxTimedSpins = 32
  3. 如果需要自旋,並且不會超時,那麼 spins = maxUntimedSpins = 512

=== 點擊查看top目錄 ===

4.10 spinForTimeoutThreshold 變量

static final long spinForTimeoutThreshold = 1000L;
...
 // 超時配置處理
 if (timed) {
     nanos = deadline - System.nanoTime();
     if (nanos <= 0L) {
         s.tryCancel();
         continue;
     }
 }
 ...
 // 自旋spins
 if (spins > 0)
     spins = shouldSpin(s) ? (spins-1) : 0;
 // 未設置超時,直接阻塞
  else if (!timed)
      LockSupport.park(this);
  // 設置超時時間阻塞
  else if (nanos > spinForTimeoutThreshold)
      LockSupport.parkNanos(this, nanos);
...
  1. timed = true ,也就是有 timeout 情況,如果 nanos <= 0L ,那麼說明已經等的夠久了,s.tryCancel(); 取消此次操作
  2. 計算超時次數 ,可以知道在有 timeout的情況下,spins = 32,也就是自旋32次之後,spins 會遞減到 0 ,緊接着走 else if (nanos > spinForTimeoutThreshold)
  3. 如果此時的超時時間仍舊還大於 1000 ns納秒,那麼就直接調用 LockSupport.parkNanos(this, nanos);避免過多的自旋 spin影響cpu 性能

=== 點擊查看top目錄 ===

4.11 tryCancel 方法

void tryCancel() {
    UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
 }
  • 實質是設置變量 match = this

4.12 clean 方法

  • java.util.concurrent.SynchronousQueue.TransferStack#clean 方法
/**
  * Unlinks s from the stack.
  */
 void clean(SNode s) {
     // item,waiter 置空
     s.item = null;   // forget item
     s.waiter = null; // forget thread
     
     // s的下一個節點處於取消操作狀態,則past指向past的下一個節點
     SNode past = s.next;
     if (past != null && past.isCancelled())
         past = past.next;

     // 頭節點被取消操作則進行將next節點更新爲頭節點
     /*
      * 這裏說一下爲什麼要:p != past ,因爲如果  past == p = head的話,
      * past就是頭節點head,那麼待會直接走下面的 while 就行,不用在這裏浪費時間,直接跳過這一步。
      */
     SNode p;
     while ((p = head) != null && p != past && p.isCancelled())
         casHead(p, p.next);

     // 頭節點調整完畢,現在將棧節點中每個節點都會進行檢查一遍,更新前後節點的關係,將取消操作的節點進行排除
     while (p != null && p != past) {
         SNode n = p.next;
         if (n != null && n.isCancelled())
             p.casNext(n, n.next);
         else
             p = n;
     }
 }
  • 這個clean 操作,不僅僅會把當前的 SNode 內部清空,也會將整個棧內失效的節點給清除掉。

=== 點擊查看top目錄 ===

五、TransferQueue 棧(隊列出入公平,先進先出)

  • 根據 debug 學習法,咱們直接看 TransferQueue 的 transfer 方法
  • 插尾巴 tail ,從頭head 取

5.1 QNode 節點

  • 匹配流程圖
    在這裏插入圖片描述
//隊列節點定義
  static final class QNode {
      // 指向隊列中的下一個節點
      volatile QNode next;          // next node in queue
      //數據域
      volatile Object item;         // CAS'ed to or from null
      //等待的線程
      volatile Thread waiter;       // to control park/unpark
      //存儲的是否是數據
      final boolean isData;
}

5.2 transfer 方法

transfer 流程圖

在這裏插入圖片描述

E transfer(E e, boolean timed, long nanos) {

	// 注意,queue結構的是 QNode,Stack結構的是 SNode
    QNode s = null; // constructed/reused as needed

    // 判斷操作是 take 還是 put 
    boolean isData = (e != null);

    for (;;) {
        QNode t = tail;
        QNode h = head;

        // 初始化 TransferQueue 的時候, head 與 tail 非 null
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin

        // h == t 說明隊列裏面壓根就沒元素,只有一個假節點  QNode h = new QNode(null, false);
        // t.isData == isData 說明隊列內的元素與新進來的類型一致。記住是 tail
        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            // 確保 t 沒問題
            if (t != tail)                  // inconsistent read
                continue;
            // tn 按理來說必須是 null,因爲 tail.next 尾巴之後肯定沒元素了,如果此時有,那麼就是剛插進來的
            if (tn != null) {               // lagging tail
            	// CAS 把 tail 指向最新的尾巴 tn 節點,
                advanceTail(t, tn);
                continue;
            }
            // 已經 timeout 超時了 
            if (timed && nanos <= 0)        // can't wait
                return null;
            // s 第一次 for 進來就是 null,那麼此時包裝成節點Node
            if (s == null)
                s = new QNode(e, isData);
            // 設置 t.next 是新進來的這個 Node
            if (!t.casNext(null, s))        // failed to link in
                continue; // CAS 失敗就 continue,這個地方不加lock 和 Synchronized,利用了 CAS 其實挺省性能的

            // ===== 前面的全部都是校驗檢查工作 =====
            // === 下面的纔是對新進來節點的處理 ===
            // 將新進來的 NodeS 插入到隊列的尾巴里面去   
            advanceTail(t, s);              // swing tail and wait

            // 一般來說,在這裏會進入阻塞
            Object x = awaitFulfill(s, e, timed, nanos);
            // 如果由於超時timeout等情況被取消,那麼會進入下面的 if
            if (x == s) {                   // wait was cancelled
                clean(t, s); 
                return null;
            }
             //匹配的操作到來,s操作完成,離開隊列,如果沒離開,使其離開(advanceHead 方法會使得 h.next = h 使得 s.isOffList() 爲 true)
            // 判斷s是否被摘掉了 (s.next =this 說明被摘掉了,也就是匹配完畢了)
            if (!s.isOffList()) {           // not already unlinked
            	// 推進head ,cas將head 由t(是t不是h),設置爲s 
            	// head 設置成 s, 摘掉 t
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

        } else {                            // complementary-mode
        	//不同的模式那麼是匹配的(put 匹配take ,take匹配put),出隊操作

        	//隊頭元素節點
            QNode m = h.next;               // node to fulfill
            //隊列發生變化,重來
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;

            // 1. 如果isData == (x != null) 爲true 那麼表示是相同的操作。 
            //(x!= null爲true說明隊列第一個元素是 put 操作, isData=true 說明是 put 操作。 x!= null 爲 false 說明第一個元素是 take 操作,isData=false說明是 take 操作 )
            //其實能進入到上面的if,然後在這個地方出現了這種情況,原因就是多線程條件下,這個線程慢了,導致隊列中狀態都變了
            // 2. x == m 則是被取消了的操作
            //m.casItem(x, e) 將 m 的 item 設置爲本次操作的 e,實際上這是最重要的匹配操作,如果失敗,那麼推進 head ,重試
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                !m.casItem(x, e)) {         // lost CAS
            	//推進head
                advanceHead(h, m);          // dequeue and retry
                continue;
            }
			//m.casItem 成功後,也要推進head
            advanceHead(h, m);              // successfully fulfilled
             //喚醒匹配操作的線程
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}
  • advanceTail(t, tn); 這個是TransferQueue 區別與 TransferStack的一個根據,這個是從尾巴插入,從頭部取,所以是先進先出。而 TransferStack 是從頭部插入,頭部讀取。後進先出。

  • 最重要的細節是:

  1. m.casItem(x, e) 將 m 的 item 設置爲本次操作的 e,實際上這是最重要的匹配操作
  2. advanceHead(h, m); 節點 s (s並沒有進隊列)與隊列 queue 內的第一個head元素 m 匹配,然後推進 head。
  3. LockSupport.unpark(m.waiter); 節點 s 與元素 m 匹配後,喚醒 m 節點
  4. 被喚醒的Node 執行 s.isOffList() 方法返回的是 true。

=== 點擊查看top目錄 ===

5.3 casItem 方法 (重要)

  • 匹配操作,牽手走人。
  • 上方調用 m.casItem(x, e) ,將 m 的 item 設置爲本次操作的 e,
boolean casItem(Object cmp, Object val) {
      return item == cmp &&
          UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
  }

public final native boolean compareAndSwapObject(Object o, long offset,
                                                     Object expected,
                                                     Object x);

5.4 clean 方法

  • java.util.concurrent.SynchronousQueue.TransferQueue#clean 方法
  • 查看上層調用:在這裏插入圖片描述
    在這裏插入圖片描述
  • 流程圖:

在這裏插入圖片描述

  • 我們注意到在TransferQueue的定義中,除了定義指向哨兵隊頭的head,隊尾的tail,還有一個屬性叫cleanMe,該屬性在clean方法裏面會被用到,主要作用是用來輔助進行清理。
  • 當TransferQueue的最後一個元素需要被移除(可能是cancel等原因導致的移除),此時該元素代表隊尾,移除動作無法直接在隊尾上面執行!
    因爲TransferQueue是單鏈表的形式實現,總不可能從頭遍歷來進行移除,一是效率,二是還要考慮超級多的併發,所以遍歷過程可能會因爲多線程併發操作導致非一致性的讀。
  • 所以這裏使用了一個叫cleanMe的內部屬性來暫存此時需要被移除的隊尾節點的前繼節點,等待下一次進入該方法的時候進行移除操作(下一次進來tail肯定發生了變化)
/**
 * Gets rid of cancelled node s with original predecessor pred.
 * 對 中斷的 或 等待超時的 節點進行清除操作
 */
void clean(QNode pred, QNode s) {
	// 1. 清除掉 thread 引用
    s.waiter = null; // forget thread                                        
    /*
     * At any given time, exactly one node on list cannot be
     * deleted -- the last inserted node. To accommodate this,
     * if we cannot delete s, we save its predecessor as
     * "cleanMe", deleting the previously saved version
     * first. At least one of node s or the node previously
     * saved can always be deleted, so this always terminates.
     *
     * 在程序運行中的任何時刻, 最後插入的節點不能被刪除(這裏的刪除指 通過 cas 直接刪除, 因爲這樣直接刪除會有多刪除其他節點的風險)
     * 當 節點 s 是最後一個節點時, 將 s.pred 保存爲 cleamMe 節點, 下次再進行清除操作
     */
    // 2. 判斷 pred.next == s, 下面的 步驟2 可能導致 pred.next = next
    while (pred.next == s) { // Return early if already unlinked           
        QNode h = head; 
        QNode hn = h.next;   // Absorb cancelled first node as head
        // 3. hn  中斷或者超時, 則推進 head 指針, 若這時 h 是 pred 則 loop 中的條件 "pred.next == s" 不滿足, 退出 loop
        if (hn != null && hn.isCancelled()) {                              
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        // 4. 隊列爲空, 說明其他的線程進行操作, 刪除了節點(注意這裏永遠會有個 dummy node)
        if (t == h)                                                        
            return;
        QNode tn = t.next;
         // 5. 其他的線程改變了 tail, continue 重新來
        if (t != tail)                                                   
            continue;
        if (tn != null) {
        	 // 6. 幫助推進 tail
            advanceTail(t, tn);                                           
            continue;
        }
        // 7. 節點 s 不是尾節點, 則 直接 CAS 刪除節點(在隊列中間進行這種刪除是沒有風險的)
        if (s != t) {        // If not tail, try to unsplice              
            QNode sn = s.next;
            if (sn == s || pred.casNext(s, sn)) // 退出循環
                return;
        }

		// 8. s 是隊列的尾節點, 則 cleanMe 出場
        QNode dp = cleanMe;                                             
        if (dp != null) {    // Try unlinking previous cancelled node
        	// 9. cleanMe 不爲 null, 進行刪除刪一次的 s節點, 也就是這裏的節點d
            QNode d = dp.next;                                          
            QNode dn;
             // 10. 這裏有幾個特殊情況:
             // 1. 原來的s節點()也就是這裏的節點d已經刪除; 
             // 2. 原來的節點 cleanMe 已經通過 advanceHead 進行刪除; 
             // 3 原來的節點 s已經刪除 (所以 !d.siCancelled), 存在這三種情況, 直接將 cleanMe 清除
            if (d == null ||               // d is gone or             
                    d == dp ||                 // d is off list or
                    !d.isCancelled() ||        // d not cancelled or
                    // 11. d 不是tail節點, 且dn沒有offlist, 直接通過 cas 刪除 上次的節點 s (也就是這裏的節點d); 其實就是根據 cleanMe 來清除隊列中間的節點
                    (d != t &&                 // d not tail and        
                            (dn = d.next) != null &&  //   has successor
                            dn != d &&                //   that is on list
                            dp.casNext(d, dn)))       // d unspliced
           		 // 12. 清除 cleanMe 節點, 這裏的 dp == pred 若成立, 說明清除節點s, 成功, 直接 return, 不然的話要再次 loop, 接着到 步驟 13, 設置這次的 cleanMe 然後再返回
                casCleanMe(dp, null);                                  
            if (dp == pred)
                return;      // s is already saved node
          // 原來的 cleanMe 是 null, 則將 pred 標記爲 cleamMe 爲下次 清除 s 節點做標識
        } else if (casCleanMe(null, pred))                         
            return;          // Postpone cleaning s
    }
}
  • 調用這個方法都是由節點線程Interrupted 或等待 timeout 時調用的, 清除時分兩種情況討論:
  1. 刪除的節點不是queue尾節點, 這時 直接 pred.casNext(s, s.next) 方式來進行刪除(和ConcurrentLikedQueue中差不多)
  2. 刪除的節點是隊尾節點
  1. 此時 cleanMe == null, 則前繼節點pred標記爲 cleanMe, 爲下次刪除做準備
  2. 此時 cleanMe != null, 先刪除上次需要刪除的節點, 然後將 cleanMe至null, 讓後再將 pred 賦值給 cleanMe。

這時我們想起了 ConcurrentSkipListMap 中的 marker 節點, 對, marker 和 cleanMe 都是起着防止併發環境中多刪除節點的功能

5.5 casCleanMe 方法

 boolean casCleanMe(QNode cmp, QNode val) {
      return cleanMe == cmp &&
          UNSAFE.compareAndSwapObject(this, cleanMeOffset, cmp, val);
  }

=== 點擊查看top目錄 ===

六、總結

  1. SynchronousQueue作爲一個無數據緩衝的阻塞隊列,其內部通過兩個內部類(隊列和棧)分別實現了公平策略和非公平策略下的隊列操作,其實我們需要記住的在於其操作必須是成雙成對的,在無超時無中斷的情況下,一個線程執行入隊操作,必然需要另一個線程執行出隊操作,此時兩操作互相匹配,同時完成操作,這也是其取名爲Synchronous(同時發生)的含義
  2. 消費者的消費速度與生產者的生產速度不一致,那麼如果是採用非公平 TransferStack,那麼棧底部的Node可能會超時或者永遠不會被讀取到。

七、番外篇

上一章節:【Java Collection】Queue 剖析(四)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章