阻塞隊列LinkedTransferQueue的初窺

前言

當我們身在分佈式開發中時經常會碰到突然大量的消息造訪,而我們的消費者無法及時處理,最終導致消息丟失,甚至服務崩潰。這個時候我們就需要暫時將這些不速之客請到“休息室”去坐一下。
阻塞隊列BlockingQueue就是我們經常使用的“休息室”。阻塞隊列可以有效的阻止大量的消息衝擊我們的服務,設置隊列大小可以將無法處理的消息阻止在外。本文將學習jdk1.7中加入的阻塞隊列--LinkedTransferQueue

一、LinkedTransferQueue簡介

1.1 命名

線性傳輸隊列 網上查不到它的合適的翻譯,我就給它取了一個名字--線性傳輸隊列。線性傳輸隊列的類繼承結構如圖所示

1.2 原理簡介

從圖中我們可以看到它底層是Collection和Queue,因此它是具有集合特性的,同時還具有Queue的基本功能。名字中還帶了Linked,說明他是鏈表形式的,然後它還引入了一個新的TransferQueue接口特性,有如下接口,接口的功能在下面講述
(1)boolean tryTransfer(E e);
(2)void transfer(E e) throws InterruptedException;
(3)boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;
(4)boolean hasWaitingConsumer();
(5)int getWaitingConsumerCount();

1.3 LinkedTransferQueue核心方法

以我目前的能力只能分析到這裏了,光分析這個就有點勉強了,awaitMatch就不分析了

    /**
     * tryTransfer相關的方法底層實現都是通過xfer實現的
     * 根據它的表現,這是一個拉和吃一體的方法,非常全能
     *
     * @param e        需要插入的信息
     * @param haveData true表示數據插入,false表示數據請求
     * @param how      操作類型,有四種類型:NOW(即時), ASYNC(異步), SYNC(同步), or TIMED(超時模式)
     * @param nanos    超時時間,單位納秒
     */
    private E xfer(E e, boolean haveData, int how, long nanos) {
        // 如果選擇了有數據要插入,但是數據又是空的,就直接拋出異常
        if (haveData && (e == null))
            throw new NullPointerException();
        // 非常複雜的一個邏輯,又沒有註釋,所以這裏的註釋都是我以爲,如果有問題請不吝賜噴
        // 最外圈的for循環是沒有限制條件的,通過循環裏面的continue restart跳轉
        restart: for (Node s = null, t = null, h = null; ; ) {
            // 這個for節點的條件有點小複雜,但是沒關係,一步步來
            // 這裏利用了java從左往右的特性
            // t != (t = tail)這句話是,先拿到t的值,此時t=null,然後再將t賦值爲tail
            // 最後做比較,null != tail,當然條件成立了,此時t已經是tail了,初始化時的tail.isData是true
            // 最後結論當haveData時(存值) p=tail,當不是haveData時(取值)p=head,符合隊列特性
            for (Node p = (t != (t = tail) && t.isData == haveData) ? t
                    : (h = head); ; ) {
                final Node q;
                final Object item;
                // 如果haveData爲true(存數據)
                // p不是數據節點且item是null,條件成立,反之不成立
                // 如果haveData爲false(取數據)
                // p是數據節點且item不是null,條件成立,反之不成立
                // 從上述說明看出:
                // 存數據時,p=tail不是數據節點進入;
                // 取數據時,p=head是數據節點進入
                // 但isData和item是一致的,所以取數據才進入這個條件,除非數據被取走了
                // 下個請求進來就直接從新的head取數據了
                if (p.isData != haveData
                        && haveData == ((item = p.item) == null)) {
                    if (h == null) h = head;
                    // 嘗試匹配,匹配成功更新p節點
                    if (p.tryMatch(item, e)) {
                        // 取數據時,一般情況下(h=head)!=(p=head)是false,
                        // 可能別的線程在這個時候有操作,h和p之間已經有被取走的節點了
                        // 可能Node還在,但是裏面的item已經是空的了,所以要將p矯正成新的頭節點
                        if (h != p) skipDeadNodesNearHead(h, p);
                        return (E) item;
                    }
                }

                // 存數據時,p是尾節點,尾節點後沒有數據了,說明這就是個尾節點,能進入條件
                // 取數據時,p是頭節點,頭結點後面如果是空的,也能進入條件
                if ((q = p.next) == null) {
                    // 如果操作方式是NOW,那麼直接返回輸入的數據
                    if (how == NOW) return e;
                    // 給s創建一個空節點,e是空的就創建請求節點,否則創建數據節點
                    if (s == null) s = new Node(e);
                    // 更新next節點成功的話,到下個循環
                    if (!p.casNext(null, s)) continue;
                    // p不是尾節點,把s更新成最新的尾節點
                    if (p != t) casTail(t, s);
                    // 這個時候ASYNC就也需要返回了
                    if (how == ASYNC) return e;
                    // 旋轉等待數據
                    return awaitMatch(s, p, e, (how == TIMED), nanos);
                }
                // 退到最外面的for循環重新開始
                if (p == (p = q)) continue restart;
            }
        }
    }

判斷是否有已經等待的消費者,通過是否有空節點來判斷

   public boolean hasWaitingConsumer() {
        restartFromHead: for (;;) {
            for (Node p = head; p != null;) {
                Object item = p.item;
                if (p.isData) {
                    // 如果隊列裏面還是有數據的
                    // 直接break
                    if (item != null)
                        break;
                }
                // 如果數據已經被人取走了,只剩下空節點了,
                // 這個時候當前線程就是等待者,然後就返回true
                else if (item == null)
                    return true;
                if (p == (p = p.next))
                    continue restartFromHead;
            }
            return false;
        }
    }

通過當前空節點數目來判斷等待的消費者數目

 /**
     * 當前等待的消費者數目
     * @return
     */
    public int getWaitingConsumerCount() {
        return countOfMode(false);
    }
    private int countOfMode(boolean data) {
        restartFromHead: for (;;) {
            int count = 0;
            for (Node p = head; p != null;) {
                // p節點是否被匹配了,當數據節點裏面的item是空的,匹配成功
                if (!p.isMatched()) {
                    // 因爲p.isData是true,所以是數據請求時,
                    if (p.isData != data)
                        return 0;
                    // 等待累加,其實就是在循環內查看被其他線程取空的數據節點有多少個
                    if (++count == Integer.MAX_VALUE)
                        break;  // @see Collection.size()
                }
                if (p == (p = p.next))
                    continue restartFromHead;
            }
            return count;
        }
    }

二、重要方法功能

通過上面的分析,我大致瞭解了LinkedTransferQueue類中幾個方法的功能

2.1 put 方法

顧名思義,這是一個存數據的方法

public void put(E e) {
        xfer(e, true, ASYNC, 0);
    }

異步存放插入數據,在tail後面插入新的節點,因爲整個數據結構是鏈表,所以是無界的,所以不會阻塞

2.2 offer 方法

offer有兩種方式,一種帶超時的,一直不帶超時的(表面上的)

 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;
    }
從源碼中很容易判斷出,這是個障眼法,因爲本身就是不會阻塞的,所以超時時間就是個擺設,設置了沒用

2.3 add 方法

和上面的offer效果是一模一樣的

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

2.4 tryTransfer 方法

有兩種,一種直接返回,一種超時返回
從上面的源碼看NOW操作方式都沒有創建新的節點,也就是不會把數據放到隊列中,直接給等待中的消費者,如果沒有等待中的,直接返回false,並且不會入隊

public boolean tryTransfer(E e) {
        return xfer(e, true, NOW, 0) == null;
    }
 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();
    }

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

2.6 take 方法

操作方式是SYNC,一直等待,直到取到數據

 public E take() throws InterruptedException {
        E e = xfer(null, false, SYNC, 0);
        if (e != null)
            return e;
        Thread.interrupted();
        throw new InterruptedException();
    }

2.7 poll 方法

有兩種方式,一種直接返回,一種帶超時時間的
直接返回的話可能就是空的數據,超時會阻塞線程,直到獲取到數據或者超時


    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() {
        return xfer(null, false, NOW, 0);
    }

2.8 getWaitingConsumerCount和hasWaitingConsumer

這兩個方法在上面已經做過源碼分析了,一個是獲取當前等待的線程數,一個是判斷當前有沒有在等待的

結語

xfer()這個方法裏面還有很多邏輯沒有弄懂,等下次完全讀懂了後再更新一下

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