Queue常用類解析之BlockingQueue(五):LinkedTransferQueue

Queue常用類解析之PriorityQueue
Queue常用類解析之ConcurrentLinkedQueue
Queue常用類解析之BlockingQueue(一):PriorityBlockingQueue、DelayQueue和DelayedWorkQueue
Queue常用類解析之BlockingQueue(二):ArrayBlockingQueue
Queue常用類解析之BlockingQueue(三):LinkedBlockingQueue
Queue常用類解析之BlockingQueue(四):SynchronousQueue

接着上文對BlockingQueue的介紹繼續向下

八、LinkedTransferQueue

1. 接口TransferQueue

TransferQueue是jdk1.7新增的一個接口,是阻塞隊列LinkedBlockingQueue的子接口。
TransferQueue適用於生產者必須等待消費者進行接收數據的阻塞隊列場景。
在普通的BlockingQueue中,入隊操作和出隊操作彼此分離,彼此都只和隊列打交道,只需要往隊列中存取數據,不需要關心對方的存在。而在TransferQueue中是一種生產者-消費者的應用場景,生產者和消費者之間並不是毫無關係的,生成者生成數據需要等待着一個消費者前來接受,否則就是一次失敗的入隊操作。因此在TransferQueue的入隊和出隊操作以外,還有一個更重要的操作概念稱爲transfer,指生產者將數據傳遞給消費者的過程。
transfer方法表示生產者傳遞數據給消費者進行消費,tryTransfer則表示在非阻塞或者限時阻塞的情況下這一過程是否成功。
TransferQueue也可以具有一個固定的容量,事實上SynchronousQueue就是一個典型的容量爲0的TransferQueue,雖然其並沒有實現TransferQueue接口,在容量爲0的TransferQueue中,只有消費者接收了數據才能put成功,因此put操作和transfer操作是同義的。
TranferQueue方法
TransferQueue定義了5個方法。
getWaitingConsumerCount和hasWaitingConsumer都與隊列中的等待的消費者有關,前者返回等待的消費者數目,後者返回是否有等待的消費者。
tranfer和兩個tryTransfer方法都用於transfer操作,分別是阻塞方法,非阻塞方法和限時阻塞方法。

2. 靜態內部類Node

final boolean isData;   // false if this is a request node
volatile Object item;   // initially non-null if isData; CASed to match
volatile Node next;
volatile Thread waiter; // null until waiting

和SychronousQueue$TransferQueue#QNode類基本一致,有4個屬性。
同樣的也有3個特殊狀態:
item值爲自身:取消狀態,表示指定時間內沒有找到匹配節點或者線程被中斷。被匹配成功時邏輯處理完成後也會將item置爲自身。
isData爲true但是item爲null:不是request節點,也不是data節點,表示一種邏輯節點,一般用於head節點
上述狀態的節點稱爲matched節點,上述狀態以外的節點爲unmatched節點,是真正需要被匹配的節點。
next值爲自身:表示節點從鏈表中移除,稱爲offList,在遍歷過程中,該類型的節點的下一個節點被視爲head節點而不是節點的next節點。
對於item屬性,request節點時item爲null,data節點時item爲數據的具體值,被成功匹配時item爲匹配節點的item值,被成功匹配時的匹配邏輯執行完成後爲節點自身,取消時爲節點自身。

2. LinkedTransferQueue簡述

LinkedTransferQueue是一個基於鏈表結果的無界的TransferQueue。
和大多數的集合不同,由於LinkedTransferQueue中存在着異步操作,因此size()方法並不是一個O(1)的方法,而是需要對集合進行遍歷,而且由於遍歷過程中隊列可能被修改,其返回的結果也並不一定是實時一致的。另外,批量方法的操作也無法保證原子性。
隊列中並不直接存儲元素,而是封裝成data節點(put)或request節點(take),當一個data節點入隊時,如果遇到request節點,進行匹配並將節點出隊,反之亦然。
由於LinkedTransferQueue是無界隊列,所以所以的入隊方法(add,put,offer)都是成功的。

/** head of the queue; null until first enqueue */
//頭結點,head.next是隊列中的第一個有效節點
transient volatile Node head;

/** tail of the queue; null until first append */
//尾節點,隊列中的最後一個有效節點
private transient volatile Node tail;

/** The number of apparent failures to unsplice removed nodes */
//unsplice方法出的失敗次數
private transient volatile int sweepVotes;

head表示第一個節點,head可能是matched節點,那麼此時head.next是第一個unmatched節點,tail最後一個有效節點(unmatch節點)。一般希望head.next,如下圖第一張。
但是LinkedTransferQueue允許更新的滯後性,存在更新的滯後性,如下圖第二張。
head&tai示意圖l
head&tai示意圖

3. LinkedTransferQueue#xfer(Object, boolean, int, long)

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

		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) { // unmatched //選取unmatch節點嘗試匹配
				if (isData == haveData)   // can't match //節點模式相同(都是request節點或者都是data節點),不能匹配,離開內循環
					break;
				if (p.casItem(item, e)) { // match 匹配成功
					for (Node q = p; q != h;) {
						Node n = q.next;  // update by 2 unless singleton
						//h依舊是head節點,說明沒有其它線程干擾,那麼從head節點開始到q都是matched節點,可以從鏈表中移除
						//以q.next(不存在則選取q)作爲新的head節點
						if (head == h && casHead(h, n == null ? q : n)) {
							//head節點修改成功,原來的head節點修改成offlist類型,因爲遍歷線程可以將head節點作爲offlist節點的遍歷過程的下一個節點
							h.forgetNext();
							break;
						}                 // advance and retry //其它線程對head節點進行了修改
						if ((h = head)   == null ||
							(q = h.next) == null || !q.isMatched())
							//head節點不存在 || head.next不存在 說明鏈表中沒有有意義的節點(request節點和data節點都不存在),不需要修改head節點
							// head.next是unmatched節點,head節點已經不需要修改
							break;        // unless slack < 2
					}
					//喚醒線程
					LockSupport.unpark(p.waiter);
					//返回被匹配節點的item屬性
					return LinkedTransferQueue.<E>cast(item);
				}
			}
			Node n = p.next; 
			p = (p != n) ? n : (h = head); // Use head if p offlist //從下個節點繼續匹配,offlist節點則以頭結點作爲下一個節點
		}

		//沒有unmatched節點或者所有節點模式相同(都是request節點或者都是data節點)
		//NOW模式不做任何處理
		if (how != NOW) {                 // No matches available
			if (s == null)
				s = new Node(e, haveData);
			//節點加入到鏈表
			Node pred = tryAppend(s, haveData);
			//加入失敗,其他線程對鏈表進行了修改,插入了可以匹配的節點,重試匹配邏輯
			if (pred == null)
				continue retry;           // lost race vs opposite mode
			if (how != ASYNC)
				//SYNC和TIMED模式還需要通過自旋/阻塞進行等待
				return awaitMatch(s, pred, e, (how == TIMED), nanos);
		}
		//NOW和ASYNC模式以及TIMED模式超時後的返回
		return e; // not waiting
	}
}

整體流程爲找到鏈表中的第一個unmatched節點,如果該節點模式是不同的,嘗試進行匹配,匹配成功後更新頭結點。如果匹配,則重試。
如果鏈表中不存在unmatched節點或者節點模式相同無法匹配,則根據參數how模式的不同進行相應的處理。

/*
 * Possible values for "how" argument in xfer method.
 */
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

xfer方法時LinkedTransferQueue的核心方法,transfer,tryTransfer以及入隊、出隊方法的邏輯都由該方法完成。
xfer方法的how參數,共4種模式:
非阻塞方法(無參的poll方法和tryTransfer方法)爲NOW模式,匹配不成功認爲失敗,不做任何處理
對於入隊的offer, put和add爲ASYNC模式,異步處理,匹配不成功加入隊列,不需要自旋/阻塞方式以等待匹配,直接返回
阻塞方法(transfer和take方法)爲SYNC模式,同步處理,匹配不成功加入隊列,匹配成功前一直自旋/阻塞
限時阻塞方法(帶時間參數的poll和tryTransfer方法)爲TIMED,匹配不成功加入隊列,匹配成功前自旋/阻塞一段時間

4. LinkedTransferQueue#tryAppend(Node, boolean)

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
		//p是null節點,將head賦給p。p爲null時有兩種場景:
		//1. tail爲null,此時head也爲null或者只有head一個節點
		//2. 在後續邏輯中,p爲offList時置爲null,即遍歷到offList節點時從head節點重新遍歷
		if (p == null && (p = head) == null) { //head節點不存在,新節點作爲head
			if (casHead(null, s))
				return s;                 // initialize
		}
		else if (p.cannotPrecede(haveData)) //tail節點爲unmatched節點且與待插入節點的模式不同,可以進行匹配
			return null;                  // lost race vs opposite mode
		else if ((n = p.next) != null)    // not last; keep traversing // p不是最後一個節點,重新確定p值
			//p != t(p從tail節點開始已經遍歷完成了一個節點)且tail節點發生了變化,從tail節點開始重新遍歷
			//否則(p剛從tail節點開始遍歷或者tail節點沒有發生變化),繼續向下遍歷。如果p是offList節點,則置爲null從頭開始(下一步的循環中會從head取值)
			//看不明白爲什麼要加 p != t的判斷,爲什麼不是tail發生變化即取 p = tail ?
			p = p != t && t != (u = tail) ? (t = u) : // stale tail 
				(p != n) ? n : null;      // restart if off list
		//CAS方式將s追加到p節點後面,失敗,繼續向下遍歷
		else if (!p.casNext(null, s))
			p = p.next;                   // re-read on CAS failure
		//s追加到p節點後面,更新tail
		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;
		}
	}
}

tryAppend方法將參數中的節點加入鏈表尾部,返回參數節點在鏈表中的前一個節點(如果參數節點作爲head節點插入鏈表,則返回參數節點自身)。
整體流程爲:
如果隊列中沒有節點,參數節點作爲head節點。
如果隊列中有節點,從tail開始遍歷到最後一個節點,如果存在可以與參數節點匹配的unmatched節點,不做處理並返回null(xfer方法調用tryAppend方法發現返回null會重試匹配)。否則將參數節點作爲最後一個節點的下一個節點,並更新tail。
在最後的更新邏輯中,可以看到是一個if語句下的循環,並不是每次都會更新tail,將該語句簡化以後如下:

if (p != t) {
	while (true) {
		if(tail == t && casTail(t, s)) {
			break;
		}
		t = tail;
		if(t == null) {
			break;
		}
		s = t.next;
		if(s == null) {
			break;
		}
		s = s.next;
		if(s == null) {
			break;
		}
		if(s == t) {
			break;
		}
	}
}

可以看到在 p == t時是不更新tail的
剩下的場景中
(1)如果tail沒有變化,更新tail節點,此時至少插入了兩個新節點
(2)如果tail發生了變化,只有在tail節點存在且tail節點後面至少有兩個非offlist的節點時更新tail節點。
綜上所述,tail節點後至少有兩個新節點纔會更新tail,而不是每次插入新節點都會更新。

5.LinekdTransferQueue#awaitMatch(Node, Node, Object, boolean, long)

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();
	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 //s.item = s, s.waiter = null。被成功匹配後最終item屬性也是自身
			return LinkedTransferQueue.<E>cast(item);
		}
		if ((w.isInterrupted() || (timed && nanos <= 0)) &&
				s.casItem(e, s)) {        // cancel //線程被中斷或超市後,進行取消
			unsplice(pred, 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 //waiter賦值,準備下一步的阻塞,waiter屬性用於被匹配後喚醒線程
		}
		else if (timed) { //限時阻塞
			nanos = deadline - System.nanoTime();
			if (nanos > 0L)
				LockSupport.parkNanos(this, nanos);
		}
		else { //永久阻塞
			LockSupport.park(this);
		}
	}
}

用於線程自旋或阻塞,直到節點被成功匹配或超時或線程中斷。
節點被成功匹配,返回匹配的節點的item值。
線程中斷或超時,節點取消,移除節點,返回節點自身的item值。

6. LinkedTransferQueue#unsplice(Node, Node)

final void unsplice(Node pred, Node s) {
	s.forgetContents(); // forget unneeded fields
	/*
	 * See above for rationale. Briefly: if pred still points to
	 * s, try to unlink s.  If s cannot be unlinked, because it is
	 * trailing node or pred might be unlinked, and neither pred
	 * nor s are head or offlist, add to sweepVotes, and if enough
	 * votes have accumulated, sweep.
	 */
	if (pred != null && pred != s && pred.next == s) { // pred節點存在且不是offlist節點且pred.next == s成立
		Node n = s.next;
		if (n == null || // s是最後一個節點
			(n != s && pred.casNext(s, n) && pred.isMatched())) { // 如果s不是最後一個節點且s不是offlist節點,則通過pred.next = s.next移除s節點。如果pred不是matched節點,return結束。否則還需要後續處理
			//嘗試更新head節點,從原來的head節點到prev(或s)之間找到第一個unmatched節點作爲新的head節點(如果沒有找到,則以最後一個matched節點作爲head節點)
			for (;;) {               // check if at, or could be, head 
				Node h = head;
				if (h == pred || h == s || h == null) //pred是head節點 || s是head節點 || head節點不存在(隊列中沒有節點),return結束
					return;          // at head or list empty
				if (!h.isMatched()) //head節點未匹配,離開循環
					break;
				Node hn = h.next;
				if (hn == null) //head節點是matched節點且head.next不存在,空隊列
					return;          // now empty
				if (hn != h && casHead(h, hn)) //更新head節點,head節點
					h.forgetNext();  // advance head
			}
			//sweepVotes計數及處理
			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)) //增加節點移除的失敗次數casSweepVotes
							break;
					}
					else if (casSweepVotes(v, 0)) {//節點移除的失敗次數casSweepVotes清0,觸發sweep方法進行全局清除
						sweep();
						break;
					}
				}
			}
		}
	}
}

unsplice方法用於將節點從鏈表中移除。

(1)prev節點或者s節點是offlist節點,不會觸發head節點更新和sweepVotes計算和處理的邏輯。
(2) 當s節點不是最後一個節點且不是offList節點,通過CAS方式移除s節點,失敗則結束方法,不會觸發head節點更新和sweepVotes計算和處理的邏輯。
(3)步驟2中通過通過CAS方式移除s節點成功,但prev節點是unmatcher節點,結束方法,不會觸發head節點更新和sweepVotes計算和處理的邏輯。
除了上述以外的場景都會進入head節點更新和sweepVotes計算和處理的邏輯。

在head節點更新的邏輯中
(1)如果pred或者s是頭結點,renturn結束,對應原來隊列中除了s節點以外沒有節點(或者都是matched節點)的場景
(2)如果遍歷到了最後一個節點,return結束,對應prev和s節點已經不再鏈表中的場景,否則遍歷到prev或s節點就會結束遍歷
除了這兩種情況外,節點更新邏輯結束之後就會進入sweepVotes計數及處理。

7. LinkedTransferQueue#sweep()

private void sweep() {
	for (Node p = head, s, n; p != null && (s = p.next) != null; ) {
		if (!s.isMatched()) //unmatched節點,跳過
			// 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; //offlist節點,從head節點重新開始遍歷
		else
			p.casNext(s, n); //matched節點,移除
	}
}

sweep節點進行全局掃描清除,移除matched節點

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