java多執行緒系列之synchronousQueue

recklessMo發表於2017-10-27

synchronousQueue詳解

在使用cachedThreadPool的時候,沒有對原理去很好的理解,所以導致使用起來有些不放心,主要是對synchronousQueue的原理不太瞭解,所以有此文的分析

本文從兩個方面分析synchronousQueue:

  1. synchronousQueue的使用,主要是通過Executors框架提供的執行緒池cachedThreadPool來講,因為synchronousQueue是它的workQueue
  2. synchronousQueue的原理,主要是從實現角度,分析一下資料結構

synchronousQueue基本使用

首先說說api吧,關於佇列有幾套api,核心是下面的兩套:

take() & put() //這是阻塞的,會阻塞操作執行緒
poll() & offer() //這是非阻塞的(在不設定超時時間的前提下),當操作不能達成的時候會立馬返回boolean複製程式碼

synchronousQueue是一個沒有資料緩衝的阻塞佇列,生產者執行緒對其的插入操作put()必須等待消費者的移除操作take(),反過來也一樣。

但是poll()和offer()就不會阻塞,舉例來說就是offer的時候如果有消費者在等待那麼就會立馬滿足返回true,如果沒有就會返回false,不會等待消費者到來。

下面我們分析一下cachedThreadPool的使用流程,通過這個過程我們來了解synchronousQueue的使用方式:先看程式碼

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {//1
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }複製程式碼
  1. 對於使用synchronousQueue的執行緒池,在第一次execute任務的時候會在1處返回false,因為執行緒池中還沒有執行緒,所以沒有消費者在等待,所以就會直接建立執行緒進行執行任務
  2. 在上篇執行緒池的分析中我們提到:建立的執行緒在執行完畢任務後會去迴圈的getTask,在getTask的過程中會呼叫take去獲取任務。所以當我們再次呼叫execute提交任務的時候1就會返回成功(前提是先前建立的執行緒已經執行完畢,正在執行gettask方法進行等待),因為這個時候已經有一個執行緒在等待task了,所以offer直接返回成功!
  3. 這就達到了cachedThreadPool執行緒複用的目的,也就是說:在提交任務的時候,如果所有工作執行緒都處於忙碌的狀態就會新建執行緒來執行,如果有工作執行緒處於空閒狀態則把任務交給空閒執行緒來執行!而這其中的黑科技就是通過synchronousQueue來進行的。

synchronousQueue內部資料結構

根據上面我們介紹的synchronousQueue的佇列語義,我們其實可以很容易的通過鎖或者訊號量等一系列的同步機制來實現一個synchronousQueue的結構,但是我們知道有鎖的一般效率都不會太高,所以java為我們提供了下面一種無鎖的演算法。

無鎖在java裡面一般就是cas和spin來實現的!具體會在之後介紹java併發包的時候來分析TODO。下面的分析的核心在於:cas和spin,一般把cas和spin可以組合起來使用,spin就是不斷迴圈重試cas操作,確保操作能夠成功。這些就不詳細介紹,網上有很多相關文章!這也是java併發的基礎!

稍微跟蹤一下程式碼,就會發現synchronousQueue內部是通過Transferer來實現的,具體分為兩個Transferer,分別是TransferStack和TransferQueue,兩者差別在於是否公平:下面我們只分析TransferQueue的實現。

        /** Node class for TransferQueue. */
        static final class QNode { //1
            volatile QNode next;          // next node in queue
            volatile Object item;         // CAS'ed to or from null
            volatile Thread waiter;       // to control park/unpark
            final boolean isData;

            QNode(Object item, boolean isData) {
                this.item = item;
                this.isData = isData;
            }

            boolean casNext(QNode cmp, QNode val) {//2
                return next == cmp &&
                    UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
            }

            boolean casItem(Object cmp, Object val) {//2
                return item == cmp &&
                    UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
            }
         }複製程式碼
  1. 既然是佇列,肯定有個node是代表佇列的節點的。
  2. 2處代表了典型的兩個cas賦值操作,代表瞭如何設定next和item的值,用於進行併發更新

之後是transfer操作

   E transfer(E e, boolean timed, long nanos) {//1
            QNode s = null; 
            boolean isData = (e != null);
            for (;;) {
                QNode t = tail;
                QNode h = head;
                if (t == null || h == null)       
                    continue;                       

                if (h == t || t.isData == isData) { //2
                    QNode tn = t.next;
                    if (t != tail)                 
                        continue;
                    if (tn != null) {               
                        advanceTail(t, tn);
                        continue;
                    }
                    if (timed && nanos <= 0)        
                        return null;
                    if (s == null)
                        s = new QNode(e, isData);//3
                    if (!t.casNext(null, s))       //4
                        continue;

                    advanceTail(t, s);              // 5
                    Object x = awaitFulfill(s, e, timed, nanos);//6
                    if (x == s) {                  
                        clean(t, s);
                        return null;
                    }

                    if (!s.isOffList()) {          
                        advanceHead(t, s);        
                        if (x != null)            
                            s.item = s;
                        s.waiter = null;
                    }
                    return (x != null) ? (E)x : e;

                } else {                           //7
                    QNode m = h.next;            //8
                    if (t != tail || m == null || h != head)
                        continue;                   

                    Object x = m.item;
                    if (isData == (x != null) ||   
                        x == m ||                  
                        !m.casItem(x, e)) {         // 9
                        advanceHead(h, m);          
                        continue;
                    }

                    advanceHead(h, m);              // 10
                    LockSupport.unpark(m.waiter);//11
                    return (x != null) ? (E)x : e;
                }
            }
        }複製程式碼
  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 (;;) {//6.1
                if (w.isInterrupted())
                    s.tryCancel(e);
                Object x = s.item;
                if (x != e)//6.2
                    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);//6.3
                else if (nanos > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanos);
            }
        }複製程式碼

在上面的程式碼中,我把重要的地方分了11步,分別進行解釋:

首先說一下大致的操作。在transfer中,把操作分為兩種,一種就是入隊put,一種是出隊take,入隊的時候會建立data節點,值為data。出隊的時候會建立一個request節點,值為null。

  1. put和take操作都會呼叫該方法,區別在於,put操作的時候e值為資料data,take操作的時候e值為null

  2. 如果h==t也就是佇列為空,或者當前佇列尾部的資料型別和呼叫該方法的資料型別一致:比如當前佇列為空,第一次來了一個入隊請求,這時候佇列就會建立出一個data節點,如果第二次又來了一個入隊請求(和第一次也就是佇列尾部的資料型別一致,都是入隊請求),這時候佇列會建立出第二個data節點,並形成一個連結串列。同理,如果剛開始來了request請求,也會入隊,之後如果繼續來了一個reqeust請求,也會繼續入隊!

  3. 滿足2的條件,就會進入3,中間會有一些一致性檢查這也是必須的,避免產生併發衝突。3會建立出一個節點,根據e值的不同,可能是data節點或者request節點。

  4. 把3中建立的節點通過cas方式設定到佇列尾部去。

  5. 把tail通過cas方式修改成3中新建立的s節點

  6. 呼叫方法awaitFulfill進行等待,如果3中建立的是data節點,那麼就會等待來一個reqeust節點,反之亦然!

    1. 放入佇列之後就開始進行迴圈判斷
    2. 終止條件是節點的值被修改,具體如果是data節點,那麼會被修改成null,如果是request節點,那麼會被修改成data值。這個修改是在第9步中由相對的請求(如果建立的是data節點,那麼就由reqeust請求來進行修改,反之亦然)來做的。如果一直沒有相對的請求過來,那麼節點的值就一直不會被修改,這樣就跳不出迴圈體!
    3. 如果沒有被修改,那麼就需要進入park休眠,等待第9步進行修改後再通過unpark進行喚醒,喚醒之後就會判斷節點值被修改從而返回。
  7. 如果在插入一個節點的時候,不滿足2的條件,也就是佇列不為空並且尾部節點和當前要插入節點的型別不一樣(這就代表來了一個相對請求),比如上圖中的尾部是data節點,如果來了一個插入reqeust節點的請求,那麼就會走到7這裡
  8. 由於是佇列,先進先出,所以會取佇列裡面的第一個節點,也就是h.nex
  9. 把8中取出的節點的值通過cas的方式設定成新來節點的e值,這樣就成功的滿足了6-2的終止條件
  10. 將head節點往後移動,這樣就把第一個節點成功的出隊。
  11. 每個節點都儲存了對應的操作執行緒,將8中節點對應的執行緒進行喚醒,這樣6-3處於休眠的執行緒就醒來了,然後繼續進行for迴圈,進而判斷6-2終止條件滿足,於是返回

整個過程就是這樣,上文的分析只是分析了正常的工作流程,沒有具體的分析操作中的競態條件,比如兩個執行緒同時進行入隊的時候如何正確設定連結串列的狀態,都講的話篇幅過大。

相關文章