逐漸深入Java多線程(三)----BlockingQueue阻塞隊列及其實現類簡介

目錄

BlockingQueue簡介

BlockingQueue的實現類

1,ArrayBlockingQueue

2,DelayQueue

3,LinkedBlockingDeque

4,LinkedBlockingQueue

5,LinkedTransferQueue

6,PriorityBlockingQueue

7,SynchronousQueue


BlockingQueue簡介

java.util.concurrent. BlockingQueue接口自JDK1.5加入java,意思是阻塞隊列,不過接口裏面的方法也不都是阻塞的,BlockingQueue提供了對阻塞隊列的多種處理方案。

 

BlockingQueue對於元素的添加,刪除,檢查等操作,分別用不同的方法提供了不同的處理方案,比如拋出異常,返回特殊值,阻塞,超時等。

BlockingQueue類的註釋中提供了這樣一份方法使用總結:

* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <caption>Summary of BlockingQueue methods</caption>
*  <tr>
*    <td></td>
*    <td ALIGN=CENTER><em>Throws exception</em></td>
*    <td ALIGN=CENTER><em>Special value</em></td>
*    <td ALIGN=CENTER><em>Blocks</em></td>
*    <td ALIGN=CENTER><em>Times out</em></td>
*  </tr>
*  <tr>
*    <td><b>Insert</b></td>
*    <td>{@link #add add(e)}</td>
*    <td>{@link #offer offer(e)}</td>
*    <td>{@link #put put(e)}</td>
*    <td>{@link #offer(Object, long, TimeUnit) offer(e, time, unit)}</td>
*  </tr>
*  <tr>
*    <td><b>Remove</b></td>
*    <td>{@link #remove remove()}</td>
*    <td>{@link #poll poll()}</td>
*    <td>{@link #take take()}</td>
*    <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
*  </tr>
*  <tr>
*    <td><b>Examine</b></td>
*    <td>{@link #element element()}</td>
*    <td>{@link #peek peek()}</td>
*    <td><em>not applicable</em></td>
*    <td><em>not applicable</em></td>
*  </tr>
* </table>

這段html的代碼展示出來是這樣的:

翻譯一下是這樣的:

注:

1,向固定長度的隊列添加元素時,官方建議用offer()方法,比add()強。

2,element()方法和peek()方法是父接口Queue中定義的,在BlockingQueue中沒有顯式定義,他們的功能是檢查隊列的頭元素,但不會刪除頭元素(這是這倆方法和offer()等方法的區別)。

 

另外,BlockingQueue還提供了下面的方法:

1,int remainingCapacity();

檢查隊列中的剩餘空間,也就是還能添加的元素數。不過,不能依靠這個方法來判斷能不能往隊列添加元素,因爲可能有多個線程正在試圖添加元素。

2,public boolean contains(Object o);

檢查隊列中是否包含某元素,用equals()方法來判斷目標對象。

3,int drainTo(Collection<? super E> c);

從BlockingQueue中刪除所有元素,並添加到參數集合中,返回添加成功的元素個數。比循環獲取元素效率高一點。

4,int drainTo(Collection<? super E> c, int maxElements);

和上面的方法相比,多出來的maxElements參數代表最多轉移的元素個數。

 

BlockingQueue的實現類

BlockingQueue的實現類默認有以下幾種:

1,ArrayBlockingQueue

JDK1.5加入。

基於數組,隊列長度固定,隊列元素符合先進先出的原則。

關於公平性:

ArrayBlockingQueue有以下幾種構造:

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    this(capacity, fair);

    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e);
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        lock.unlock();
    }
}

其中的fair參數就是公平性的設置,默認爲false。

ArrayBlockingQueue的很多方法使用ReentrantLock作爲鎖,比如put()方法,offer()方法等,如果公平性設置爲true,則先申請鎖的線程會先獲得鎖,也即是一種先進先出的策略。開啓公平性會降低吞吐效率,好處是降低了變數,同時避免飢餓(有的線程總是拿不到鎖)。

 

2,DelayQueue

JDK1.5加入

延遲隊列,基於PriorityQueue(優先級隊列),是一個建立在優先級基礎上,並且元素有延遲時間的隊列。

DelayQueue中只能放實現了Delayed接口的對象,這種對象一般需要實現兩個方法:

long getDelay(TimeUnit unit);

int compareTo(T o);

分別是獲得延遲剩餘時間的方法和比較排序用的方法。

Delayed接口的代碼:

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

只有一個getDelay()方法,用於獲得元素的剩餘延遲時間,當延遲時間小於等於0時,標識延遲時間已過,可以被操作。

另外Delayed接口繼承了Comparable接口:

public interface Comparable<T> {

    public int compareTo(T o);
}

也是隻有一個接口,用於排序。

從上面兩個接口可以看到,可以向DelayQueue中存放的元素都有延遲時間,而且放入隊列中時會被排序,不一定是先進先出了。

 

下面以offer()方法爲例,看看DelayQueue 新增元素時是怎麼排序的:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e);
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

除了使用鎖之外,代碼調用了變量q的offer()方法,變量q就是DelayQueue中維護的優先級隊列:

private final PriorityQueue<E> q = new PriorityQueue<E>();

使用的是PriorityQueue的無參構造,PriorityQueue類本身維護了一個Comparator,無參構造中的Comparator是null。

PriorityQueue的offer()方法是這樣的:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

可以看到當隊列裏沒有元素的時候就直接把元素放在隊列的第一個位置,否則調用siftUp()方法:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

根據隊列是否有comparator,來決定是用PriorityQueue的comparator還是用元素自己的compareTo()方法。根據上面的代碼我們看到,DelayQueue中的PriorityQueue不維護comparator,所以我們看一下siftUpComparable()方法:

private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

代碼中使用了元素的compareTo()方法,用以排序。

 

下面以take()方法爲例,看看DelayQueue 獲得元素時是怎麼考慮延遲時間的:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek();
            if (first == null)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return q.poll();
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

可以看到,方法首先用peek()方法獲取隊列第一個元素,然後調用元素的getDelay()方法獲得延遲剩餘時間,如果剩餘時間小於等於0,則調用poll()方法獲取元素。

 

3,LinkedBlockingDeque

JDK1.6加入。

基於雙向鏈表,可以指定隊列長度,如果不指定隊列長度則上限爲Integer.MAX_VALUE。此隊列的鏈接節點在每次插入時都會動態創建。

LinkedBlockingDeque和其他阻塞隊列略有不同,他的鏈表是雙向的,也就是鏈表兩頭都可進行出隊或者入隊,因爲他實現的是BlockingDeque接口,BlockingDeque接口不但繼承了BlockingQueue接口,還同時繼承了Deque接口,Deque接口分別定義了頭部和尾部的出入隊方法。

如果我們在使用他的時候總是在隊尾出隊或入隊,實際的效果就像一個棧一樣。

 

簡單介紹一下Deque接口,這個接口定義了一個雙向列表,所謂deque,就是double ended queue的縮寫,直譯是雙尾隊列,實際上這個隊列還是有明確的頭和尾的,說雙尾只是說在兩頭都能出入隊,因此在這個接口中定義的方法都要指明是在隊首操作還是在隊尾操作。

Deque接口的註釋給我們總結了相關方法的功能,也是在註釋中寫的html代碼,翻譯後是這樣的:

 

下面介紹一下BlockingDeque接口,這個接口同時繼承了BlockingQueue接口和Deque接口,而且在Deque接口的基礎上擴展了幾個方法,BlockingDeque接口的方法彙總如下:

理論上雙向鏈表提供的功能是可以覆蓋單向鏈表的,畢竟人家兩頭都能操作,BlockingDeque接口在註釋中還貼心的介紹了BlockingQueue的方法在BlockingDeque中的等價方法:

其實也很好理解,入隊走隊尾,出隊走隊首,檢查查隊首,隊列的設計本來就是這樣的。

 

LinkedBlockingDeque還提供了其他幾個方法:

removeFirstOccurrence(Object o)

removeLastOccurrence(Object o)

來自BlockingDeque接口。

作用是從隊列中刪除一個元素,這兩個方法分別從隊列頭開始向後遍歷,或者從隊尾向前遍歷,刪除後返回true。

 

pop(),同removeFirst()

push(E e),同addFirst(e)

上面兩個方法都來自Deque接口,在Deque接口中就同時存在這兩個同義方法,不知道爲什麼要做這樣重複的方法定義。

 

Iterator<E> iterator()

Iterator<E> descendingIterator()

來自Deque接口,分別提供了從隊首到隊尾,和從隊尾到隊首的迭代器。

 

Spliterator<E> spliterator()

JDK1.8加入,得到的是基於Spliterator的可分割迭代器。

 

4,LinkedBlockingQueue

JDK1.5加入。

基於鏈表,隊列元素符合先進先出的原則。可以指定隊列長度,如果不指定隊列長度則上限爲Integer.MAX_VALUE。

此隊列的出隊和入隊用的鎖是分開的。

線程池中的Executors. newFixedThreadPool() 使用了這個隊列。

 

5,LinkedTransferQueue

JDK1.7加入。

基於鏈表,隊列長度不限,操作基本不靠加鎖,靠CAS來實現。

這是一個很有特點的隊列,名字中的Transfer是轉交的意思,這種轉交和SynchronousQueue挺像,不過操作更豐富一點,在後面的介紹中可以加深對這個名字的理解。

LinkedTransferQueue的特點主要來源於兩方面,一是隊列節點的設計,二是出入隊規則的設計,下面分別來看。

 

節點設計:

LinkedTransferQueue的節點維護了以下幾個屬性:

1,final boolean isData

節點類型。該隊列的節點有兩種類型:數據節點(isData==true)和請求節點(isData==false),數據節點是入隊操作時添加的節點,這個操作和其他隊列一樣,而請求節點是出隊操作時添加的節點,沒錯,LinkedTransferQueue的出隊操作有可能會往隊列中添加一個節點。

2,volatile Object item

隊列中的元素。數據節點的item是非null的,請求節點的item是null。和其他隊列不同之處在於,這個元素是有可能發生改變的,入隊操作發現請求節點時從null變成非null,出隊操作發現數據節點時從非null變成null,另外這種變化是CAS操作。

3,volatile Node next

節點的下一節點,和其他基於鏈表的隊列基本相同。

4,volatile Thread waiter

等待線程。

 

出入隊規則:

看完上面的節點設計大概就能猜到LinkedTransferQueue是怎麼玩的了,而他的出入隊規則大概描述一下就是這樣的:

入隊操作時,查找隊列中第一個請求節點(由出隊操作生成),把入隊的item放入請求節點的item參數,然後喚醒請求節點的等待線程。如果此時隊列中沒有請求節點,則按照入隊時的處理方案來處理。

出隊操作時,查找隊列中第一個數據節點(由入隊操作生成),數據節點的item設爲null,喚醒數據節點的等待線程(如果有)。如果此時隊列中沒有數據節點,則按照入隊時的處理方案來處理。

總結一下就是:使用對稱操作,出隊入隊都是查找與自己不同類型的節點,查到了就交換item,喚醒等待線程,查不到就按既定方案處理(既定方案在後面解釋)。

 

LinkedTransferQueue的這種出入隊規則是基於Dual Queue算法稍作修改而來,隊列中既可以存放數據節點又可以存放請求節點的策略來自於此。

在Dual Queue算法的基礎上,LinkedTransferQueue使用了一種鬆弛策略,因而匹配成功的節點不一定會立即出隊,犧牲了一點遍歷隊列的效率,好處是減少了CAS操作的競爭開銷。

 

其他隊列在出隊入隊時提供的處理方案往往是:拋出異常,返回特定值,阻塞,和超時,而LinkedTransferQueue提供的處理方案有:

1,NOW。​​​​​​

立刻返回結果,不論成功失敗,不會阻塞。此方案的入隊方法只有在隊列中包含請求節點時纔會返回成功,出隊方法也只有在隊列中包含數據節點時纔會返回成功。

poll()方法(出隊),tryTransfer()方法(入隊),用到了這個方案。

2,ASYNC

異步操作。由於此隊列長度不限,所以異步入隊基本上是必然成功的,而此隊列的出隊可以通過增加請求節點(佔座)的方式完成,基本上也是必然成功的,所以此方案下的出入隊基本都會成功。

offer()方法(出隊),put()方法(入隊),add()(入隊)方法用到了這個方案。可以看到,除了LinkedTransferQueue特有的transfer()和tryTransfer()外,入隊方法使用的都是這種方案。

3,SYNC

同步操作。阻塞線程直到成功爲止。

transfer()方法(入隊),take()方法(出隊)用到了這個方案。

4,TIMED

超時方案。阻塞線程直到操作成功。

poll()方法,tryTransfer()方法(帶超時時間的重載版)使用了這個方案。

 

類中對這幾種處理方案有以下的常量定義:

/*
 * Possible values for "how" argument in xfer method.
 */
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
private static final int TIMED = 3; // for timed poll, tryTransfer

根據上面的內容,我們就可以理解LinkedTransferQueue中Transfer的含義。其轉交的概念在transfer()和tryTransfer()這兩個入隊方法中得到了體現,這兩個方法只有在隊列中包含請求節點時纔會操作成功,而在這種場景下,請求節點的線程會讓請求節點立即出隊,表現出的效果就是要入隊的元素基本沒在隊列裏呆過,入隊線程把元素直接交給了出隊線程,也就是所謂的轉交。

 

和SynchronousQueue的區別:

LinkedTransferQueue和SynchronousQueue的套路非常像,都用了雙向鏈表,SynchronousQueue是在線程鏈表中排隊轉交,而LinkedTransferQueue是在本隊列中排隊轉交。

LinkedTransferQueue在Dual Queue的基礎上用了鬆弛策略,匹配到的節點不一定立即出隊,SynchronousQueue使用的就是Dual Queue策略,匹配到了就一起出隊。

SynchronousQueue至少還用了一把鎖,也用到了CAS操作,LinkedTransferQueue基本是靠CAS操作來完成的。

LinkedTransferQueue比SynchronousQueue更通用,也更強大,速度據說也更快。

LinkedTransferQueue提供立即返回結果的接口transfer()和tryTransfer(),不阻塞,不等超時。

 

另外值得指出的是,LinkedTransferQueue中的出隊入隊方法,包括獨有的transfer()和tryTransfer(),代碼實現上都調用了同一個方法: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

        for (Node h = head, p = h; p != null;) { // find & match first node
            boolean isData = p.isData;
            Object item = p.item;
            if (item != p && (item != null) == isData) { // unmatched
                if (isData == haveData)   // can't match
                    break;
                if (p.casItem(item, e)) { // match
                    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
                    }
                    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)
                s = new Node(e, haveData);
            Node pred = tryAppend(s, haveData);
            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
    }
}

方法的幾個參數代表的含義是:

E e

要處理的元素節點。入隊操作此參數爲null。

boolean haveData

操作是否有數據,入隊操作此參數爲true,出隊操作爲false。

int how

處理方案,包括NOW,ASYNC,SYNC,TIMED。

long nanos

超時時間,處理方案是TIMED時會用到。

 

關於LinkedTransferQueue的更詳細介紹,網上的這篇文章是非常好的:

https://segmentfault.com/a/1190000016460411

 

6,PriorityBlockingQueue

JDK1.5加入。

基於數組,默認容量是11。最大容量是Integer.MAX_VALUE – 8。

不允許null值。隊列中的元素會按照指定排序方式排序。同優先級元素不保證順序。

不可比較的元素無法加入此隊列。

 

排序算法基於二叉堆,二叉堆的數據結構類似完全二叉樹或近似完全二叉樹。當我們用數組表示二叉堆時,數組中的節點有這樣的特點:假設一個節點在數組中的位置是n,那麼他在二叉堆中左孩子節點在數組中的位置是2*n+1,右孩子節點的位置是2*n+2,而他的父節點在數組中的位置是(n-1)>>1

 

二叉堆添加節點的邏輯:

1,在隊尾添加節點。

2,節點和父節點比較,如果父節點比較自己小,則兩個節點交換位置。

3,重複第二步,繼續比較上級父節點,直到不能繼續交換爲止。

源碼如下(無指定Compartor):

private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0)
            break;
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

其中參數int k是要加入的節點位置,剛開始添加時肯定是隊尾,參數T x是要添加的元素,參數Object[] array是二叉堆的數組。

 

二叉堆刪除節點的邏輯:

1,從隊首刪除節點,隊首出現空位。確認隊尾節點。

2,用隊尾節點比較空位的左右兩個子節點,如果隊尾節點小,隊尾節點直接放入空位,刪除節點邏輯結束,否則比較其左右兩個子節點,小的子節點放入空位。

3,重複第二步直到隊尾節點放入正確的空位。

源碼如下(無指定Compartor):

private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = array[child];
            int right = child + 1;
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            if (key.compareTo((T) c) <= 0)
                break;
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}

其中參數int k是要空節點下標,剛開始刪除時肯定是隊首,參數T x是隊尾元素,在邏輯的最後要放入最終的空位。

參數Object[] array是二叉堆的數組,參數int n是數組長度。

 

因爲是基於數組的隊列,其擴容方法可以關注一下:

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

可以看到,當前容量小於64時,擴容後容量是oldCap*2+2,當前容量大於64時,擴容後容量是oldCap*1.5,容量小的時候擴容快,也就是這段:

int newCap = oldCap + ((oldCap < 64) ?
                       (oldCap + 2) : // grow faster if small
                       (oldCap >> 1));

另外擴容後如果超過了最大容量,新容量就變爲最大容量Integer.MAX_VALUE – 8。

 

7,SynchronousQueue

JDK1.5加入。

隊列無容量,新增元素時必須等待另一個線程的刪除操作才能新增成功。反之亦然。

因爲沒有容量,peek(),clear()等方法是沒有意義的。

不允許null值。

等待中的Producer和Consumer使用同一把鎖,節點和item轉換靠CAS來實現。

線程池中的Executors.newCachedThreadPool() 使用了這個隊列。

出隊和入隊的策略和LinkedTransferQueue很像,用到了對稱操作,put(),take()等方法中調用的都是TransferQueue或TransferStack的transfer(E e, boolean timed, long nanos)方法,根據第一個參數e是否是null來區分是出隊還是入隊。就像是LinkedTransferQueue 的xfer()方法一樣。

SynchronousQueue可使用公平策略或非公平策略,默認非公平策略:

public SynchronousQueue() {
    this(false);
}

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

 

公平策略

使用公平策略時,隊列使用TransferQueue,這是SynchronousQueue中的內部類,雙向隊列,底層是鏈表,符合先進先出(FIFO)的原則。

TransferQueue中定義了QNode類作爲節點類,並維護了head節點和tail節點,不過這兩個節點不屬於隊列中,他們的next節點纔是隊列的首尾節點。

TransferQueue類的transfer()方法:

E transfer(E e, boolean timed, long nanos) {

    QNode s = null; // constructed/reused as needed
    boolean isData = (e != null);

    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin

        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            if (t != tail)                  // inconsistent read
                continue;
            if (tn != null) {               // lagging tail
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)        // can't wait
                return null;
            if (s == null)
                s = new QNode(e, isData);
            if (!t.casNext(null, s))        // failed to link in
                continue;

            advanceTail(t, s);              // swing tail and wait
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s);
                return null;
            }

            if (!s.isOffList()) {           // not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

        } else {                            // complementary-mode
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }

            advanceHead(h, m);              // successfully fulfilled
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

出隊入隊用的都是這個方法,用參數e來判斷是出隊還是入隊,在方法中描述了幾個場景:

1,隊列爲空,或tail節點類型和自己一致,則嘗試入隊。先要判斷tail是否改變,改變了說明有其他線程正在操作,所以要循環重來,然後判斷tail是否有next節點,有next說明存在其他線程添加了同類節點,需要更新tail節點然後循環重新重來,前面判斷都通過了就新建一個節點作爲tail的next節點,然後修改tail節點(這兩個操作就可能讓此線程會成爲前面所謂的其他線程),阻塞此線程並等待被喚醒或超時。

2,tail節點類型和自己不一致,則嘗試出隊。獲得head的next節點(不是head節點,head節點不在隊列中),判斷節點是否取消,然後交換數據,喚醒對方線程,把對方節點設爲head節點(head節點不會被匹配,匹配的是head的next節點)。

 

非公平策略

使用非公平策略時,隊列使用TransferStack。這是SynchronousQueue中的內部類,雙向棧,底層是鏈表,符合後進先出(LIFO)的原則。

TransferStack中SNode類作爲節點類,並維護了head參數作爲棧頂。

TransferStack的transfer()方法:

E transfer(E e, boolean timed, long nanos) {

    SNode s = null; // constructed/reused as needed
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
        SNode h = head;
        if (h == null || h.mode == mode) {  // empty or same-mode
            if (timed && nanos <= 0) {      // can't wait
                if (h != null && h.isCancelled())
                    casHead(h, h.next);     // pop cancelled node
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled
                    clean(s);
                    return null;
                }
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   // help unlink
                }
            }
        } else {                            // help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

出隊入隊用的都是這個方法,用參數e來判斷是出隊還是入隊,在方法中描述了幾個場景:

1,棧爲空,或棧頂元素和本次請求類型相同(比如都是入隊請求或者都是出隊請求),則判斷超時後嘗試入棧。

2,棧頂元素和本次請求類型不同,即匹配的場景,而且棧頂元素沒有在匹配,則開始與棧頂元素匹配,調用tryMatch()方法嘗試匹配,匹配成功後喚醒對方線程,然後兩個節點一起出棧。

3,棧頂元素和本次請求類型不同,但棧頂元素正在匹配,則幫助棧頂元素進行匹配,然後循環重試。

所以,雖說SynchronousQueue容量爲0,但是隊列中依然維護了一個鏈表,保存了阻塞的線程列表。

 

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