併發程式設計—— LinkedTransferQueue

weixin_34148340發表於2018-04-30

1. 前言

Java 中總的算起來有 8 種阻塞佇列。

我們分析了:

ArrayBlockingQueue 陣列佇列,我們在 使用 ReentrantLock 和 Condition 實現一個阻塞佇列 看過了 JDK 寫的一個例子,就是該類的基本原理和實現。樓主不準備分析了。

LinkedBlockingDeque是一個雙向連結串列的佇列。常用於 “工作竊取演算法”,有機會再分析。

DelayQueue 是一個支援延時獲取元素的無界阻塞佇列。內部用 PriorityQueue 實現。有機會再分析。

PriorityBlockingQueue 是一個支援優先順序的無界阻塞佇列,和 DelayWorkQueue 類似。有機會再分析。

今天要分析的是剩下的一個比較有意思的佇列:LinkedTransferQueue

為什麼說有意思呢?他可以算是 LinkedBolckingQueueSynchronousQueue 和合體。

我們知道 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. 總結

LinkedTransferQueueSynchronousQueueLinkedBlockingQueue 的合體,效能比 LinkedBlockingQueue 更高(沒有鎖操作),比 SynchronousQueue能儲存更多的元素。

put 時,如果有等待的執行緒,就直接將元素 “交給” 等待者, 否則直接進入佇列。

puttransfer 方法的區別是,put 是立即返回的, transfer 是阻塞等待消費者拿到資料才返回。transfer方法和 SynchronousQueue的 put 方法類似。

相關文章