阻塞佇列 LinkedTransferQueue 原始碼解析

槑!發表於2020-10-16

LinkedTransferQueue

  • LinkedTransferQueue是下面三者的集合體

    • LinkedBlockingQueue(支援阻塞,即失敗入隊,但不再支援獲取 Lock 的阻塞了,因為是基於 cas 的)
    • SynchronousQueue(公平模式,實現了 Transfer 介面)
    • ConcurrentLinkedQueue (支援非阻塞,因為傳統的阻塞佇列都是使用的 Lock,想要 offer 或者 poll 等操作必須先獲得 lock,而非阻塞是 直接cas 一次,失敗就直接返回,不用阻塞獲得鎖)
  • 四種模式

    • NOW,立即返回,沒有匹配到立即返回,不做入隊操作

      • 對應的方法有:poll()、tryTransfer(e)
    • ASYNC,非同步,元素入隊但當前執行緒不會阻塞(相當於無界LinkedBlockingQueue的元素入隊)

      • 對應的方法有:add(e)、offer(e)、put(e)、offer(e, timeout, unit)
    • SYNC,同步,元素入隊後當前執行緒阻塞,等待被匹配到

      • 對應的方法有:take()、transfer(e)
    • TIMED,有超時,元素入隊後等待一段時間被匹配,時間到了還沒匹配到就返回元素本身

      • 對應的方法有:poll(timeout, unit)、tryTransfer(e, timeout, unit)
  • 底層:所有方法最終呼叫的都是 xfer(),這個方法和 synchronousQueue 的 transfer 方法一樣,都是基於 cas 無鎖的,會先檢查是否符合匹配規則,即佇列所有節點是否是資料節點和請求不同,如果不同的話會遍歷匹配,遍歷是為了防止併發下被其他執行緒先匹配了,如果請求和佇列都是資料節點或者都不是資料節點,則不能進行匹配的話,如果是 Now 模式就立即返回失敗了,其他模式會把該模式的這個請求入隊,入隊採用 cas 保證一定成功,在入隊成功之後如果是 Async 模式(即入隊請求),那麼也就直接返回了,剩下的 Time 和 Sync 模式,會阻塞等待匹配的結果返回,即使用 LockSupport 定時阻塞,然後檢查 item 是否發生了交換,發生了就返回

屬性
public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {
        // 頭節點
        transient volatile Node head;
        // 尾節點
        private transient volatile Node tail;
        // 放取元素的幾種方式:
        // 立即返回,用於非超時的poll()和tryTransfer()方法中
        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
        // 超時,用於有超時的poll()和tryTransfer()方法中
        private static final int TIMED = 3; // for timed poll, tryTransfer


        static final class 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
        }
    
    	// ...
構造方法
public LinkedTransferQueue() {
}

public LinkedTransferQueue(Collection<? extends E> c) {
    this();
    addAll(c);
}
入隊

四個方法都是一樣的,使用 非同步的方式 呼叫xfer()方法,因為是無界佇列,當然不會等待了

public void put(E e) {
    // 非同步模式,不會阻塞,不會超時
    // 因為是放元素,單連結串列儲存,會一直往後加
    xfer(e, true, ASYNC, 0);
}

public boolean offer(E e, long timeout, TimeUnit unit) {
    xfer(e, true, ASYNC, 0);
    return true;
}

public boolean offer(E e) {
    xfer(e, true, ASYNC, 0);
    return true;
}

public boolean add(E e) {
    xfer(e, true, ASYNC, 0);
    return true;
}
出隊

出隊的四個方法也是直接或間接的呼叫 xfer() 方法,只是模式不同

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
public E take() throws InterruptedException {
    // 同步模式,會阻塞直到取到元素
    E e = xfer(null, false, SYNC, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 有超時時間
    E e = xfer(null, false, TIMED, unit.toNanos(timeout));
    if (e != null || !Thread.interrupted())
        return e;
    throw new InterruptedException();
}

public E poll() {
    // 立即返回,沒取到元素返回null
    return xfer(null, false, NOW, 0);
}

移交元素
public boolean tryTransfer(E e) {
    // 立即返回
    return xfer(e, true, NOW, 0) == null;
}

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

public boolean tryTransfer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    // 有超時時間
    if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
        return true;
    if (!Thread.interrupted())
        return false;
    throw new InterruptedException();
}

xfer()
/**
  * e:表示元素;
  * haveData:表示是否是資料節點,
  * how:表示放取元素的方式,上面提到的四種,NOW、ASYNC、SYNC、TIMED;
  * nanos:表示超時時間;
  */
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
            // p節點的模式
            boolean isData = p.isData;
            // p節點的值
            Object item = p.item;
            
            // p 沒有被取消,再校驗一下是否已經被其他執行緒匹配
            if (item != p && (item != null) == isData) { // unmatched
                // 如果兩者都是資料節點或者都不是資料節點,則不能匹配,直接挑出迴圈後嘗試入隊
                if (isData == haveData)   // can't match
                    break;
                
                // 否則一個資料節點,一個非資料節點,則嘗試匹配,即 cas 更新 item
                if (p.casItem(item, e)) { // match
                    // 沒被其他執行緒競爭,匹配成功,下面更新 head,即下一次要匹配的節點。
                    for (Node q = p; q != h;) {
                        // 當前節點是 q ,下一個要匹配的節點是 q.next
                        Node n = q.next;  // update by 2 unless singleton
                        // head 已被當前執行緒匹配,將其更新為下一個
                        if (head == h && casHead(h, n == null ? q : n)) {
                            h.forgetNext();
                            break;
                        }                 // advance and retry
                        // 更新 head 失敗會執行下面
                        // 如果新的頭節點為空,或者其next為空,或者其next未匹配,就重試
                        if ((h = head)   == null ||
                            (q = h.next) == null || !q.isMatched())
                            break;        // unless slack < 2
                    }
                    // 喚醒p中等待的執行緒
                    LockSupport.unpark(p.waiter);
                    // 並返回匹配到的元素
                    return LinkedTransferQueue.<E>cast(item);
                }
            }
            
            // 執行到這說明, p 已經被匹配了或者嘗試匹配的時候失敗了
            Node n = p.next;
            p = (p != n) ? n : (h = head); // Use head if p offlist
        }

        /**
         * 到這裡肯定是佇列中儲存的節點型別和自己一樣,或者佇列中沒有元素了,那麼就入隊(不管放元素還是取元素都得入隊)
         * 入隊又分成四種情況
         */
        // 如果不是立即返回
        if (how != NOW) {                 // No matches available
            // 新建s節點
            if (s == null)
                s = new Node(e, haveData);
            // 嘗試入隊
            Node pred = tryAppend(s, haveData);
            // 入隊失敗,重試
            if (pred == null)
                continue retry;           // lost race vs opposite mode
            // 如果不是非同步(同步或者有超時),就等待匹配,與返回匹配的結果
            // 該方法同 SynchrnousQueue,會呼叫 LookSupport 定時阻塞,然後檢查當前節點的 item 屬性是否發聲了交換
            if (how != ASYNC)
                return awaitMatch(s, pred, e, (how == TIMED), nanos);
        }
        return e; // not waiting
    }
}

LinkedTransferQueue與SynchronousQueue(公平模式)有什麼異同呢?

(1)在java8中兩者的實現方式基本一致,都是使用的雙重佇列;

(2)前者完全實現了後者,但比後者更靈活;

(3)後者不管放元素還是取元素,如果沒有可匹配的元素,所在的執行緒都會阻塞;

(4)前者可以自己控制放元素是否需要阻塞執行緒,比如使用四個新增元素的方法就不會阻塞執行緒,只入隊元素,使用transfer()會阻塞執行緒;

(5)取元素兩者基本一樣,都會阻塞等待有新的元素進入被匹配到;

相關文章