1. 前言
Java 中總的算起來有 8 種阻塞佇列。
我們分析了:
- 併發程式設計之 SynchronousQueue 核心原始碼分析
- 併發程式設計之 ConcurrentLinkedQueue 原始碼剖析
- 併發程式設計之 LinkedBolckingQueue 原始碼剖析
- 在 併發程式設計 —— ScheduledThreadPoolExecutor 中順帶分析了 DelayWorkQueue。
ArrayBlockingQueue
陣列佇列,我們在 使用 ReentrantLock 和 Condition 實現一個阻塞佇列 看過了 JDK 寫的一個例子,就是該類的基本原理和實現。樓主不準備分析了。
LinkedBlockingDeque
是一個雙向連結串列的佇列。常用於 “工作竊取演算法”,有機會再分析。
DelayQueue
是一個支援延時獲取元素的無界阻塞佇列。內部用 PriorityQueue
實現。有機會再分析。
PriorityBlockingQueue
是一個支援優先順序的無界阻塞佇列,和 DelayWorkQueue
類似。有機會再分析。
今天要分析的是剩下的一個比較有意思的佇列:LinkedTransferQueue
。
為什麼說有意思呢?他可以算是 LinkedBolckingQueue
和 SynchronousQueue
和合體。
我們知道 SynchronousQueue
內部無法儲存元素,當要新增元素的時候,需要阻塞,不夠完美,LinkedBolckingQueue
則內部使用了大量的鎖,效能不高。
兩兩結合,豈不完美?效能又高,又不阻塞。
我們一起來看看。
2. LinkedTransferQueue 介紹
該類實現了一個 TransferQueue。該介面定義了幾個方法:
public interface TransferQueue<E> extends BlockingQueue<E> {
// 如果可能,立即將元素轉移給等待的消費者。
// 更確切地說,如果存在消費者已經等待接收它(在 take 或 timed poll(long,TimeUnit)poll)中,則立即傳送指定的元素,否則返回 false。
boolean tryTransfer(E e);
// 將元素轉移給消費者,如果需要的話等待。
// 更準確地說,如果存在一個消費者已經等待接收它(在 take 或timed poll(long,TimeUnit)poll)中,則立即傳送指定的元素,否則等待直到元素由消費者接收。
void transfer(E e) throws InterruptedException;
// 上面方法的基礎上設定超時時間
boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;
// 如果至少有一位消費者在等待,則返回 true
boolean hasWaitingConsumer();
// 返回等待消費者人數的估計值
int getWaitingConsumerCount();
}
複製程式碼
相比較普通的阻塞佇列,增加了這麼幾個方法。
3. 關鍵原始碼分析
阻塞佇列不外乎put ,take,offer ,poll
等方法,再加上TransferQueue
的 幾個 tryTransfer
方法。我們看看這幾個方法的實現。
put
方法:
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
複製程式碼
take
方法:
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
複製程式碼
offer
方法:
public boolean offer(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
複製程式碼
poll
方法:
public E poll() {
return xfer(null, false, NOW, 0);
}
複製程式碼
tryTransfer
方法:
public boolean tryTransfer(E e) {
return xfer(e, true, NOW, 0) == null;
}
複製程式碼
transfer
方法:
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
複製程式碼
可怕,所有方法都指向了xfer
方法,只不過傳入的不同的引數。
第一個引數,如果是 put
型別,就是實際的值,反之就是 null。
第二個引數,是否包含資料,put 型別就是 true,take 就是 false。
第三個引數,執行型別,有立即返回的NOW
,有非同步的ASYNC
,有阻塞的SYNC
, 有帶超時的 TIMED
。
第四個引數,只有在 TIMED
型別才有作用。
So,這個類的關鍵方法就是 xfer 方法了。
4. xfer 方法分析
原始碼加註釋:
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
// head 的型別。
boolean isData = p.isData;
// head 的資料
Object item = p.item;
// item != null 有 2 種情況,一是 put 操作, 二是 take 的 itme 被修改了(匹配成功)
// (itme != null) == isData 要麼表示 p 是一個 put 操作, 要麼表示 p 是一個還沒匹配成功的 take 操作
if (item != p && (item != null) == isData) {
// 如果當前操作和 head 操作相同,就沒有匹配上,結束迴圈,進入下面的 if 塊。
if (isData == haveData) // can't match
break;
// 如果操作不同,匹配成功, 嘗試替換 item 成功,
if (p.casItem(item, e)) { // match
// 更新 head
for (Node q = p; q != h;) {
Node n = q.next; // update by 2 unless singleton
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
} // advance and retry
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
// 喚醒原 head 執行緒.
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)
// 建立一個 node
s = new Node(e, haveData);
// 嘗試將 node 追加對佇列尾部,並返回他的上一個節點。
Node pred = tryAppend(s, haveData);
// 如果返回的是 null, 表示不能追加到 tail 節點,因為 tail 節點的模式和當前模式相反.
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
}
}
複製程式碼
程式碼有點長,其實邏輯很簡單。
邏輯如下:
找到 head
節點,如果 head
節點是匹配的操作,就直接賦值,如果不是,新增到佇列中。
注意:佇列中永遠只有一種型別的操作,要麼是 put
型別, 要麼是 take
型別.
整個過程如下圖:
相比較 SynchronousQueue
多了一個可以儲存的佇列,相比較 LinkedBlockingQueue
多了直接傳遞元素,少了用鎖來同步。
效能更高,用處更大。
5. 總結
LinkedTransferQueue
是 SynchronousQueue
和 LinkedBlockingQueue
的合體,效能比 LinkedBlockingQueue
更高(沒有鎖操作),比 SynchronousQueue
能儲存更多的元素。
當 put
時,如果有等待的執行緒,就直接將元素 “交給” 等待者, 否則直接進入佇列。
put
和 transfer
方法的區別是,put 是立即返回的, transfer 是阻塞等待消費者拿到資料才返回。transfer
方法和 SynchronousQueue
的 put 方法類似。