背景:
一個BlockingQueue的是一個這樣的隊列,每個插入操作都必須等待另一個刪除操作,反過來也一樣。一個同步隊列沒有內部容量這個概念。你不能使用peek操作,因爲一個元素僅在你試着刪除它的時候才能夠被取得。你不能插入一個元素(任何方法),直到另一個線程試着刪除它。你不能迭代它們,因爲沒有東西可以被迭代。queue的頭元素head是第一個進入隊列中的元素,並且插入線程正在爲它等待。如果隊列中沒有像以上的元素存在,那麼調用poll的方法會返回null。對於Collection的其他方法(比如contains),SynchronousQueue表現得像一個空的集合。它不允許null入隊。
這個隊列類似於CSP和Ada中使用的會合信道。它們適合於切換的設計,比如一個線程中的對象必須同步等待另一個線程中運行的對象從而傳遞一些信息/事件/任務。
這個類支持可選的公平策略從而制訂生產者和等待者的等待順序。默認情況下,這個順序是沒有保證的,使用true可以確保隊列是FIFO的。
這個類以及它的迭代器實現了某些Collection和Iterator中的方法。
算法:
這個算法實現了雙重棧和雙重隊列算法。
(LIfo)棧用作非公平模式,(Fifo)隊列用於公平模式。這兩者的性能相似。Fifo經常能在競爭情況下提供更高的吞吐量,但是Lifo能夠在一般應用中維持更高的線程局部性。
雙重隊列(以及相似的棧)在任何時刻都是持有“數據”--item通過put操作提供或者“請求”--slo通過take操作提供,或者爲空。一個調用試着“滿足”(一個請求的調用得到數據或者一個數據的調用匹配了請求)結果是出隊了一個模式互補的節點。最有趣的的地方在於任何操作都能夠明確當前隊列處於哪種模式,以及表現得好像不需要鎖。
隊列和棧都擴展了抽象的Transferer接口,它定義了唯一一個transfer方法用於put或者take。這些方法統一在一個方法下是因爲在雙重數據結構中,put和take方法是對稱的兩種方法,所以幾乎所有代碼可以被組合。結果transfer方法是比較長的,但是這樣相對於把他們分成幾乎重複的幾部分代碼還是更好的。
這個隊列和棧共享了很多相似的概念但是很少的具體細節。爲了簡單性,他們保持不同從而在後續可以分開演化。
這個同步隊列的算法不同於以前的算法,包括消除的機制。主要的不同在於:
- 其他的算法使用位-掩碼的方式,但是現在的節點直接使用了模式位,從而導致了其他的不同。
- 同步隊列必須等待直到被其他線程來“滿足”。
- 提供了超時和中斷的支持,包括從鏈表中清理節點/線程從而避免垃圾數據的持有和內存消耗。
- 不能放入null元素,否則拋出NullPointerException異常。
- 不限時版本(timed==false),返回非null爲成功插入,返回null拋出InterruptedException異常。
- 限時版本(timed==true),返回非null爲成功插入,返回null的確情況下,若處於中斷狀態則拋出InterruptedException異常,否則表明超時。
/**
* Puts or takes an item.
*/
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);
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;
advanceTail(t, s); // swing tail and wait
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
if (!s.isOffList()) { // not already unlinked
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
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 cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}
advanceHead(h, m); // successfully fulfilled
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
這裏的基本算法是循環試着執行下列行動之一:
- 如果隊列是空的或者擁有和當前調用同樣的模式,試着在隊列中增加等待節點,等待這個節點被滿足(或者取消),然後返回相匹配的數據。
- 如果隊列不爲空以及隊列擁有和當前調用相補的模式,試着去通過CAS操作來改變等待節點的item域,彈出隊列,以及返回相匹配的數據。
- 取得當前調用的模式isData,進入循環,首先排除head和tail都爲null的情況。
- 當隊列爲空(head=tail)或者隊尾節點的模式和當前調用模式相同的情況下:假如tail元素已經改變則更新重來,假如當前是限時版本並且時間參數不大於0則返回null,創建新節點並且以CAS方式入隊,更新tail節點,然後等待相補的調用的來臨(awaitFulfill),假如返回的數據是節點本身(說明是中斷或者超市)則做清理,並返回null,否則假如節點還在隊列中則更新head元素並且修改當前節點的item和waiter域,然後返回獲得的x和e中非空的元素(此處必定是1空1非空)。
- 否則,隊列中有相補模式的節點,則試着取得隊首元素即head.next,嘗試滿足他(fulfill),假如失敗則重試,成功之後會遞增head的值,然後喚醒(unpark)等待線程以及返回相匹配的數據。
接着我們來看阻塞操作awaitFulfill:
/**
* Spins/blocks until node s is fulfilled.
*
* @param s the waiting node
* @param e the comparison value for checking match
* @param timed true if timed wait
* @param nanos timeout value
* @return matched item, or s if cancelled
*/
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* Same idea as TransferStack.awaitFulfill */
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(e);
Object x = s.item;
if (x != e)
return x;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)
--spins;
else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
這裏的s是根據調用構造的節點,e爲傳遞的數據(null是請求,否則數據),timed爲限時標示,nanos時間量(納秒):
- 首先我們根據timed取得結束時間,假如非限時則爲0,然後取得線程對象以及自旋次數:隊首元素&非限時(maxUntimedSpins最大);隊首元素&限時(maxTimedSpins次之);否則爲0。
- 進入循環:假如中斷則嘗試取消節點,假如item變化則返回數據,假如限時版本並且超時則嘗試取消並重試或者更新nanos,遞減自旋次數直到0----->設置節點中的線程對象----->調用非限時掛起版本或者根據閾值判斷是否調用限時掛起版本。
我們最後看transfer方法中的清理操作:
void clean(QNode pred, QNode s) {
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.
*/
while (pred.next == s) { // Return early if already unlinked
QNode h = head;
QNode hn = h.next; // Absorb cancelled first node as 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)
continue;
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))
return;
}
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))
return; // Postpone cleaning s
}
}
方法中的pred爲s入隊時的前驅,這個方法中註釋裏有如下說明:
在任一時刻,只有一個鏈表中的節點不能被刪除---最後一個插入的節點。爲了遷就這個原則,如果我們無法刪除s,那麼我們就保存它的前驅節點爲“cleanMe”,首先刪除之前保存的版本。所以至少s或者之前保存的節點能夠被刪除,所以最後總是能夠被刪除!
詳情如下:
- 首先既然s已經被取消,則設置它的等待者waiter爲null,進入循環(條件:pred.next爲s推斷出s不爲head,s爲head的話不需要刪除了)。
- 取得head和tail,探測他們是否停滯,過於停滯時更新他們的值。
- 當s節點不爲隊尾節點時候(嘗試更新前驅pred,或者當s處於OffList時返回)。
- s節點爲尾元素:假如cleanMe不爲空,則說明有之前並未刪除的尾節點,那麼則嘗試刪除之前的cleanMe之後的節點,否則嘗試設置當前的pred爲cleanMe,等待下一次的刪除,然後返回。
if (dp == pred)
這一行在我看來是沒必要的,因爲永遠不可能出現。
我們接着分析stack:
stack
/**
* Puts or takes an item.
*/
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
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())
casHead(h, h.next); // pop cancelled node
else
return null;
} else if (casHead(h, s = snode(s, e, h, mode))) {
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // wait was cancelled
clean(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)) { // try to fulfill
if (h.isCancelled()) // already cancelled
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
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)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
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
}
}
}
}
這裏的Node實際上擁有三種狀態:REQUEST/DATA/FULFILLING,基本算法是循環試着執行下列3種行動之一:
- 如果棧爲空或者包含相同模式的節點,試着入棧然後等待匹配,返回匹配數據,在取消時返回null。
- 如果包含相補的節點,試着入棧一個fulfill模式的節點,匹配相對應的等待節點,彈出這兩個節點,返回匹配數據。這裏的匹配或者取消鏈接操作可能沒有被實際執行,因爲第三種行動:
- 如果棧頂元素爲fulfill模式的節點,嘗試幫助它執行match以及pop操作,然後再重試。這裏的代碼和fulfill的行爲幾乎相同的,只不過不返回數據。
- 取得當前調用的模式mode,REQUEST或則DATA,然後進入循環。
- 取得棧頂元素h,當隊列爲空或者棧頂元素模式與當前模式相同時:首先排除限時調用和nanos不大於0的情況,然後返回null。否則鏈接到h,並嘗試入棧,成功之後通過awaitFulfill等待相補調用的來臨,然後根據返回SNode節點是否爲s本身來判斷是否需要清理操作,假如獲取了數據那麼便協助更新head,以及根據當前模式返回數據。
- 當前棧頂元素與當前調用模式不同,那麼假如當前棧頂元素模式不爲fulfill時(通過 (mode & FULFILLING) != 0來判斷),進入循環,s爲當前棧頂元素,m爲下一個待匹配元素(必定不爲null),嘗試取得mn(m.next),試着使用m.tryMatch(s),來完成m和s的匹配,並且傳遞s到了m.match(注意,這一步可能也會由另一種情況完成),成功之後改變head值,以及返回當前數據或者匹配數據。假如失敗(意味着最後探測到的match不爲s,唯一的場景爲等待線程中斷或者超時),則重新設置s的next值爲mn,然後重試。
- 當前棧頂元素的模式爲fulfill時,嘗試取得棧頂元素h和next節點m,然後試着匹配m和h的值,這裏的作用幾乎和上一種情況中類似,只不過不返回數據,以及只協助一次。
/**
* Spins/blocks until node s is matched by a fulfill operation.
*
* @param s the waiting node
* @param timed true if timed wait
* @param nanos timeout value
* @return matched node, or s if cancelled
*/
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.tryCancel();
SNode m = s.match;
if (m != null)
return m;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
這個操作事實上和隊列版本中的類似,首先來解釋下注釋中的內容:當一個節點/線程試着去阻塞,它會在設置waiter域之後至少檢查一次狀態,然後纔會調用parking(阻塞),這樣子可以通過waiter從而和它的滿足者協作從而確保不會丟失信號。如果當前調用的幾點位於棧頂,那麼在park之前會首先嚐試自旋,這樣可以在生產者和消費者非常接近時避免阻塞。但是這個只在多核處理器下才會有用。從代碼中的檢查情況可以看出,在優先級上,中斷狀態--->正式返回---->超時。(所以最後一個檢查是用來探測超時的)除了非限時的同步隊列。{poll/offer}方法不會檢查中斷以及等待太久,所以對於中斷和超時的判斷被放置於transfer方法中,這樣要好於調用awaitFulfill。詳情如下(與隊列版本中類似):取得當前的結束時間,當前線程,以及自旋次數。然後進入循環。首先判斷是否中斷,判斷限時版本下的時間流逝,判斷自旋,以及根據當前節點s所處的位置來設置自旋次數。設置線程對象(用於喚醒)。最後根據是否限時來阻塞當前線程,限時版本下會根據閾值來判斷是否需要阻塞。最後我們來看處理中斷和超時情況下的清理操作clean:
void clean(SNode s) {
s.item = null; // forget item
s.waiter = null; // forget thread
/*
* At worst we may need to traverse entire stack to unlink
* s. If there are multiple concurrent calls to clean, we
* might not see s if another thread has already removed
* it. But we can stop when we see any node known to
* follow s. We use s.next unless it too is cancelled, in
* which case we try the node one past. We don't check any
* further because we don't want to doubly traverse just to
* find sentinel.
*/
SNode past = s.next;
if (past != null && past.isCancelled())
past = past.next;
// Absorb cancelled nodes at head
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
// Unsplice embedded nodes
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}
這裏的s爲被取消了的節點,這裏的註釋有如下說明:
- 設置s節點的item和waiter都爲null,因爲已經不需要了,並且它的狀態可以由match爲this來判斷。
- 取得s的下一個節點past,假如past也是取消的,那麼再取下一節點。
- 從頭p=head開始,逐步斷開past之前的那些被取消的節點。
- 再從p開始刪除嵌在棧中的節點,知道棧爲空或者找到哨兵節點(past)。