【Java 併發筆記】7 種阻塞佇列相關整理

weixin_34208283發表於2019-01-15

文前說明

作為碼農中的一員,需要不斷的學習,我工作之餘將一些分析總結和學習筆記寫成部落格與大家一起交流,也希望採用這種方式記錄自己的學習之旅。

本文僅供學習交流使用,侵權必刪。
不用於商業目的,轉載請註明出處。

1. 簡介

  • 阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。
  • 兩個附加的操作。
    • 支援阻塞的 插入 方法,在佇列為空時,獲取元素的執行緒會等待佇列變為非空。
    • 支援阻塞的 移除 方法,當佇列滿時,儲存元素的執行緒會等待佇列可用。
  • 阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。
    • 阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。
  • BlockingQueue 是一個繼承自 Queue 的介面,在 Queue 的佇列基礎上增加了阻塞操作。

Queue 介面

  • 入隊、出隊和檢索均有兩種實現,區別在於其中一種會在方法執行失敗時丟擲異常,而另一種會在失敗時返回值。
    • 以入隊為例,add() 方法在佇列已滿時將會丟擲異常,而 offer() 在佇列已滿時會返回 false。
入隊 出隊 檢索 處理方式
add() remove() element() 在執行方法失敗時不返回值,丟擲異常。
offer() poll() peek() 在執行方法時,給出返回值,比如 false、null。

BlockingQueue 介面

  • BlockingQueue 在 Queue 介面的基礎上對入隊和出隊兩個操作分別又增加了阻塞方法。
    • 設定了時間的入隊和出隊操作,在時間到了之後如果還未執行成功,那麼返回 false 和 null。
    • put()take() 方法會一直阻塞。
阻塞入隊 阻塞出隊 定時入隊 定時出隊
put(E e) E take() offer(E e,long timeout,TimeUnit unit) E poll(long timeout,TimeUnit unit)

2 Java 中的阻塞佇列

  • JDK 中提供了 7 個阻塞佇列。
阻塞佇列 說明
ArrayBlockingQueue 一個由陣列結構組成的有界阻塞佇列。
LinkedBlockingQueue 一個由連結串列結構組成的有界阻塞佇列。
PriorityBlockingQueue 一個支援優先順序排序的無界阻塞佇列。
DelayQueue 一個使用優先順序佇列實現的無界阻塞佇列。
SynchronousQueue 一個不儲存元素的阻塞佇列。
LinkedTransferQueue 一個由連結串列結構組成的無界阻塞佇列。
LinkedBlockingDeque 一個由連結串列結構組成的雙向阻塞佇列。

ArrayBlockingQueue

  • ArrayBlockingQueue 是一個用 陣列 實現的 有界 阻塞佇列。
    • 此佇列按照先進先出(FIFO)的原則對元素進行排序。
    • 在建立物件時必須指定容量大小。
    • 預設情況下不保證訪問者公平的訪問佇列,所謂公平訪問佇列是指阻塞的所有生產者執行緒或消費者執行緒,當佇列可用時,可以按照阻塞的先後順序訪問佇列,即先阻塞的生產者執行緒,可以先往佇列裡插入元素,先阻塞的消費者執行緒,可以先從佇列裡獲取元素。通常情況下為了保證公平性會降低吞吐量。
  • 在建立 ArrayBlockingQueue 時,可以設定物件的內部鎖是否採用公平鎖,預設採用 非公平鎖
  • 公平的阻塞佇列,使用了 重入鎖(ReentrantLock) 實現。
ArrayBlockingQueue fairQueue = new  ArrayBlockingQueue(1000,true);
......
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();
}
  • 在 ArrayBlockingQueue 內部,維護了一個 定長陣列(items),以便快取佇列中的資料物件,在生產者放入資料和消費者獲取資料,都是共用同一個鎖物件(lock),在插入或刪除元素時不會產生或銷燬任何額外的物件例項。

LinkedBlockingQueue

  • LinkedBlockingQueue 是一個用 連結串列 實現的 有界 阻塞佇列。
    • 此佇列的預設和最大長度為 Integer.MAX_VALUE。
    • 此佇列按照先進先出的原則對元素進行排序。
    • 建立物件時如果不指定容量大小,則使用預設大小。
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
}
  • 同 ArrayListBlockingQueue 類似,其內部也維持著一個資料緩衝佇列(該佇列 由一個連結串列構成
  • 當生產者往佇列中放入一個資料時,佇列會從生產者手中獲取資料,並快取在佇列內部,而生產者立即返回。
  • 只有當佇列緩衝區達到最大值快取容量時(LinkedBlockingQueue 可以通過建構函式指定該值),才會阻塞生產者佇列,直到消費者從佇列中消費掉一份資料,生產者執行緒會被喚醒,反之對於消費者這端的處理也基於同樣的原理。
  • LinkedBlockingQueue 之所以能夠高效的處理併發資料,還因為其對於生產者端和消費者端分別採用了獨立的鎖來控制資料同步,這也意味著在高併發的情況下生產者和消費者可以並行地操作佇列中的資料,以此來提高整個佇列的併發效能。
  • 值得注意的是,如果構造一個 LinkedBlockingQueue 物件,而沒有指定其容量大小,LinkedBlockingQueue 會預設一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到佇列滿阻塞產生,系統記憶體就有可能已被消耗殆盡了。
  • 在插入或刪除元素時會生成一個額外的 Node 物件。

PriorityBlockingQueue

  • PriorityBlockingQueue 是基於 優先順序 的阻塞佇列(優先順序的判斷通過建構函式傳入的 Compator 物件決定),需要注意的是 PriorityBlockingQueue 並 不會阻塞資料生產者,而只會在沒有可消費的資料時,阻塞資料的消費者。
    • 因此使用的時候要特別注意,生產者生產資料的速度絕對不能快於消費者消費資料的速度,否則時間一長,會最終耗盡所有的可用堆記憶體空間。
  • 在實現 PriorityBlockingQueue 時,內部控制執行緒同步的鎖採用的是 公平鎖
public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
}

DelayQueue

  • DelayQueue 是一個支援 延時獲取元素無界 阻塞佇列。
    • 佇列使用 PriorityQueue 來實現。
    • 佇列中的元素必須實現 Delayed 介面,在建立元素時可以指定多久才能從佇列中獲取當前元素。
    • 只有在延遲期滿時才能從佇列中提取元素。
  • DelayQueue 是一個沒有大小限制的佇列,因此往佇列中插入資料的操作(生產者)永遠不會被阻塞,而只有獲取資料的操作(消費者)才會被阻塞。

Deque 介面

  • Deque 是一個雙端佇列,既支援在隊頭執行入隊出隊,也支援在隊尾執行出隊。
    • 而 Queue 只支援在隊頭出隊,在隊尾入隊。
隊頭入隊 隊頭出隊 隊尾入隊 隊尾出隊 隊頭檢索 隊尾檢索 處理方式
addFirst() removeFirst() addLast() removeLast() getFirst() getLast() 在方法執行失敗時會丟擲異常
offerFirst() pollFirst() offerLast() pollLast() peekFirst() peekLast() 在方法執行失敗時會返回 false 或者 null。

  • DelayQueue 可以運用在以下應用場景。
    • 快取系統的設計。
      • 使用 DelayQueue 儲存快取元素的有效期,使用一個執行緒迴圈查詢 DelayQueue,一旦能從 DelayQueue 中獲取元素時,表示快取有效期到了。
    • 定時任務排程。
      • 使用 DelayQueue 儲存當天將會執行的任務和執行時間,一旦從 DelayQueue 中獲取到任務就開始執行,比如 TimerQueue 就是使用 DelayQueue 實現的。

SynchronousQueue

  • SynchronousQueue 是一種 無緩衝 的等待佇列,類似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿著產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,如果一方沒有找到合適的目標,那麼都在集市等待。
    • 相對於有緩衝的 BlockingQueue 來說,少了一箇中間經銷商的環節(緩衝區),如果有經銷商,生產者直接把產品批發給經銷商,而無需在意經銷商最終會將這些產品賣給那些消費者,由於經銷商可以庫存一部分商品,因此相對於直接交易模式,總體來說採用中間經銷商的模式會吞吐量高一些(可以批量買賣)。
    • 但另一方面,又因為經銷商的引入,使得產品從生產者到消費者中間增加了額外的交易環節,單個產品的及時響應效能可能會降低。
  • 宣告一個 SynchronousQueue 有兩種不同的方式,它們之間有著不太一樣的行為。
    • 如果採用公平模式,SynchronousQueue 會採用公平鎖,並配合一個 FIFO 佇列來阻塞多餘的生產者和消費者,從而體現整體的公平策略。
    • 如果是非公平模式(SynchronousQueue 預設),SynchronousQueue 採用非公平鎖,同時配合一個 LIFO 佇列來管理多餘的生產者和消費者,而後一種模式,如果生產者和消費者的處理速度有差距,則很容易出現飢渴的情況,即可能有某些生產者或者是消費者的資料永遠都得不到處理。
  • SynchronousQueue 非常適合於傳遞性場景,比如在一個執行緒中使用的資料,傳遞給另外一個執行緒使用,SynchronousQueue 的吞吐量高於 LinkedBlockingQueue 和 ArrayBlockingQueue。

LinkedTransferQueue

  • LinkedTransferQueue 是一個由 連結串列 結構組成的 無界 阻塞 TransferQueue 佇列。
  • 相對於其他阻塞佇列 LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
    • transfer 方法。如果當前有消費者正在等待接收元素(消費者使用 take() 方法或帶時間限制的 poll() 方法時),transfer 方法可以把生產者傳入的元素立刻 transfer(傳輸)給消費者。
      • 如果沒有消費者在等待接收元素,transfer 方法會將元素存放在佇列的 tail 結點,並等到該元素被消費者消費了才返回。
    • tryTransfer 方法。則是用來試探下生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回 false。和 transfer 方法的區別是 tryTransfer 方法無論消費者是否接收,方法立即返回。而 transfer 方法是必須等到消費者消費了才返回。
    • 對於帶有時間限制的 tryTransfer(E e, long timeout, TimeUnit unit) 方法,則是試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回 false,如果在超時時間內消費了元素,則返回 true。

LinkedBlockingDeque

  • LinkedBlockingDeque 是一個由 連結串列 結構組成的 雙向 阻塞佇列。
    • 所謂雙向佇列指的可以從佇列的兩端插入和移出元素。
  • 雙端佇列因為多了一個操作佇列的入口,在多執行緒同時入隊時,也就減少了一半的競爭。
  • BlockingDeque 在 Deque 的基礎上增加了阻塞的方法。

BlockingDeque 介面

阻塞隊頭入隊 阻塞隊頭出隊 阻塞隊尾入隊 阻塞隊尾出隊 處理方式
putFirst(E e) E takeFirst() putLast(E e) E takeLast() 沒有超時設定
offerFirst(E e,long timeout,TimeUnit unit) E pollFirst(long timeout,TimeUnit unit) offerLast(E e,long timeout,TimeUnit unit) E pollLast(long timeout,TimeUnit unit) 在超時之後,返回 false 或者 null。
  • 以 First 單詞結尾的方法,表示插入,獲取(peek)或移除雙端佇列的第一個元素。
  • 以 Last 單詞結尾的方法,表示插入,獲取或移除雙端佇列的最後一個元素。
  • 另外插入方法 add 等同於 addLast,移除方法 remove 等效於 removeFirst。

  • 在初始化 LinkedBlockingDeque 時可以初始化佇列的容量,用來防止其再擴容時過渡膨脹。

3. 阻塞佇列的實現原理

3.1 BlockingQueue

  • BlockingQueue 中不允許有 null 元素,因此在 add()offer()put() 時如果引數是 null,會丟擲空指標。null 是用來有異常情況時做返回值的。
public interface BlockingQueue<E> extends Queue<E> {
    //新增失敗時會丟擲異常
    boolean add(E e);

    //新增失敗時會返回 false
    boolean offer(E e);

    //新增元素時,如果沒有空間,會阻塞等待;可以響應中斷
    void put(E e) throws InterruptedException;

    //新增元素到佇列中,如果沒有空間會等待引數中的時間,超時返回,會響應中斷
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //獲取並移除隊首元素,如果沒有元素就會阻塞等待
    E take() throws InterruptedException;

    //獲取並移除隊首元素,如果沒有就會阻塞等待引數的時間,超時返回
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    //返回佇列中剩餘的空間
    int remainingCapacity();

    //移除佇列中某個元素,如果存在的話返回 true,否則返回 false
    boolean remove(Object o);

    //檢查佇列中是否包含某個元素,至少包含一個就返回 true
    public boolean contains(Object o);

    //將當前佇列所有元素移動到給定的集合中,這個方法比反覆地獲取元素更高效
    //返回移動的元素個數
    int drainTo(Collection<? super E> c);

    //移動佇列中至多 maxElements 個元素到指定的集合中
    int drainTo(Collection<? super E> c, int maxElements);
}

3.2 ArrayBlockingQueue

  • ArrayBlockingQueue 使用可重入鎖 ReentrantLock 實現的訪問公平性,兩個 Condition 保證了新增和獲取元素的併發控制。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    //使用陣列儲存的元素
    final Object[] items;
    //下一次取元素的索引
    int takeIndex;
    //下一次新增元素的索引
    int putIndex;
    //當前佇列中元素的個數
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    //全部操作的鎖
    final ReentrantLock lock;
    //等待獲取元素的鎖
    private final Condition notEmpty;
    //等待新增元素的鎖
    private final Condition notFull;
    //...
}

建構函式

//指定佇列的容量,使用非公平鎖
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();
}
//允許使用一個 Collection 來作為佇列的預設元素
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) {    //遍歷新增指定集合的元素
                if (e == null) throw new NullPointerException();
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;    //修改 putIndex 為 c 的容量 +1
    } finally {
        lock.unlock();
    }
}
  • 有三種建構函式
    • 預設的建構函式只指定了佇列的容量,設定為非公平的執行緒訪問策略。
    • 第二種建構函式中,使用 ReentrantLock 建立了 2 個 Condition 鎖。
    • 第三種建構函式可以在建立佇列時,將指定的元素新增到佇列中。

add 方法

  • add(E) 呼叫了父類的方法,而父類裡呼叫的是 offer(E),如果返回 false 就丟擲異常。
public boolean add(E e) {
    return super.add(e);
}
//super.add() 的實現
public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

offer 方法

  • offer(E) 方法先拿到鎖,如果當前佇列中元素已滿,就立即返回 false。
    • 如果沒滿就呼叫 enqueue(E) 入隊。
public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}
  • enqueue(E) 方法會將元素新增到陣列佇列尾部。
    • 如果新增元素後佇列滿了,就修改 putIndex 為 0。
    • 新增後呼叫 notEmpty.signal() 通知喚醒阻塞在獲取元素的執行緒。
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length) putIndex = 0;
    count++;
    notEmpty.signal();
}

put 方法

  • put() 方法可以響應中斷(lockInterruptibly),當佇列滿了,就呼叫 notFull.await() 阻塞等待,等有消費者獲取元素後繼續執行, 可以新增時還是呼叫 enqueue(E)
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

offer(E,long,TimeUnit) 方法

public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    if (e == null) throw new NullPointerException();
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}
  • offer()put() 方法很相似,不同之處在於允許設定等待超時時間,超過這麼久如果還不能有位置,就返回 false;否則呼叫 enqueue(E),然後返回 true。

poll 方法

  • poll() 如果在佇列中沒有元素時會立即返回 null,如果有元素呼叫 dequeue()
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}
  • 預設情況下 dequeue() 方法會從隊首移除元素(即 takeIndex 位置)。
  • 移除後會向後移動 takeIndex,如果已經到隊尾,則歸零。
  • 結合前面新增元素時的歸零,可以看到,其實 ArrayBlockingQueue 是個環形陣列。
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length) takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}
  • 然後呼叫 itrs. elementDequeued(),這個 itrs 是 ArrayBlockingQueue 的內部類 Itrs 的物件,是個迭代器,它的作用是保證迴圈陣列迭代時的正確性。

take 方法

  • take() 方法可以響應中斷,與 poll() 不同的是,如果佇列中沒有資料會一直阻塞等待,直到中斷或者有元素,有元素時還是呼叫 dequeue() 方法。
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

poll(long,TimeUnit) 方法

  • 帶引數的 poll() 方法相當於無參 poll()take() 的中和版,允許阻塞一段時間,如果在阻塞一段時間還沒有元素進來,就返回 null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}

peek 方法

  • peel() 直接返回陣列中隊尾的元素,並不刪除元素。
    • 如果佇列中沒有元素返回的是 null。
public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        lock.unlock();
    }
}
final E itemAt(int i) {
    return (E) items[i];
}

總結

  1. 一旦建立,則容量不能再改動
  2. 這個類是執行緒安全的,並且迭代器也是執行緒安全的。
  3. 這個類的 puttake 方法分別會在佇列滿了和佇列空了之後被阻塞操作。
  4. 這個類提供了 offerpoll 方法來插入和提取元素,而不會在佇列滿了或者佇列為空時阻塞操作。
  5. 這個佇列的鎖預設是 不公平 策略,即喚醒執行緒的順序是不確定的。

3.3 LinkedBlockingQueue

  • LinkedBlockingQueue 中有 兩個 ReentrantLock,一個用於新增另一個用於獲取,這和 ArrayBlockingQueue 不同。
//連結串列結點
static class Node<E> {
    E item;

    //對當前結點的後一個結點,有三種情況:
    //1.真正的結點
    //2.當前結點本身,說明當前結點是頭結點
    //3.null,說明這個結點是尾結點
    Node<E> next;

    Node(E x) { item = x; }
}

//當前容量,預設是 Integer.MAX_VALUE
private final int capacity;
//當前佇列中的元素數量
private final AtomicInteger count = new AtomicInteger();
//佇列中的頭結點,頭結點的.item 永遠為 null
transient Node<E> head;
//佇列的尾結點,尾結點的 next 永遠為 null
private transient Node<E> last;

//獲取元素的鎖
private final ReentrantLock takeLock = new ReentrantLock();
//等待取元素的等待佇列
private final Condition notEmpty = takeLock.newCondition();
//新增元素的鎖
private final ReentrantLock putLock = new ReentrantLock();
//等待新增元素的等待佇列
private final Condition notFull = putLock.newCondition();

建構函式

//使用 Integer.MAX_VALUE 作為容量
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

//指定最大容量
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

//使用 Integer.MAX_VALUE 作為容量,同時將指定集合新增到佇列中
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {    //遍歷新增到佇列
            if (e == null)    //需要注意待新增集合中不能有空值
                throw new NullPointerException();
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

put 方法

  • LinkedBlockingQueue 使用了 AtomicInteger 型別的 count 儲存佇列元素個數,在新增時,如果佇列滿了就阻塞等待。
    • 有兩種繼續執行的情況。
      • 有消費者取元素,count 會減少,小於佇列容量。
      • 或者呼叫了 notFull.signal() 。
  • ArrayBlockingQueue 中放入資料阻塞的時候,需要消費資料才能喚醒。
    • 而 LinkedBlockingQueue 中放入資料阻塞的時候,因為內部有 2 個鎖,可以並行執行放入資料和消費資料,不僅在消費資料的時候進行喚醒插入阻塞的執行緒,同時在插入的時候如果容量還沒滿,也會喚醒插入阻塞的執行緒。
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException(); // 不允許空元素
    int c = -1;
    Node<E> node = new Node(e); // 以新元素構造結點
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); // 放鎖加鎖,保證呼叫put方法的時候只有1個執行緒
    try {
        while (count.get() == capacity) { // 如果容量滿了
            notFull.await(); // 阻塞並掛起當前執行緒
        }
        enqueue(node); // 結點新增到連結串列尾部
        c = count.getAndIncrement(); // 元素個數+1
        if (c + 1 < capacity) // 如果容量還沒滿
            notFull.signal(); // 在放鎖的條件物件notFull上喚醒正在等待的執行緒,表示可以再次往佇列裡面加資料了,佇列還沒滿
    } finally {
        putLock.unlock(); // 釋放放鎖,讓其他執行緒可以呼叫put方法
    }
    if (c == 0) // 由於存在放鎖和拿鎖,這裡可能拿鎖一直在消費資料,count會變化。這裡的if條件表示如果佇列中還有1條資料
        signalNotEmpty(); // 在拿鎖的條件物件notEmpty上喚醒正在等待的1個執行緒,表示佇列裡還有1條資料,可以進行消費
}
  • 入隊呼叫的 enqueue() 連結串列尾部新增結點。
private void enqueue(Node<E> node) {
    last = last.next = node;
}
  • 在入隊後,還會判斷一次佇列中的元素個數,如果此時小於佇列容量,喚醒其他阻塞的新增執行緒。
  • 最後還會判斷容量,如果這時佇列中有 1 個元素的情況,就通知 notEmpty 上阻塞的執行緒。
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

offer 方法

  • offer 直接返回結果。
public boolean offer(E e) {
    if (e == null) throw new NullPointerException(); // 不允許空元素
    final AtomicInteger count = this.count;
    if (count.get() == capacity) // 如果容量滿了,返回false
        return false;
    int c = -1;
    Node<E> node = new Node(e); // 容量沒滿,以新元素構造結點
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 放鎖加鎖,保證呼叫offer方法的時候只有1個執行緒
    try {
        if (count.get() < capacity) { // 再次判斷容量是否已滿,因為可能拿鎖在進行消費資料,沒滿的話繼續執行
            enqueue(node); // 結點新增到連結串列尾部
            c = count.getAndIncrement(); // 元素個數+1,並返回舊值
            if (c + 1 < capacity) // 如果容量還沒滿
                notFull.signal(); // 在放鎖的條件物件notFull上喚醒正在等待的執行緒,表示可以再次往佇列裡面加資料了,佇列還沒滿
        }
    } finally {
        putLock.unlock(); // 釋放放鎖,讓其他執行緒可以呼叫offer方法
    }
    if (c == 0) // 如果佇列中還有1條資料
        signalNotEmpty(); // 在拿鎖的條件物件notEmpty上喚醒正在等待的1個執行緒,表示佇列裡還有1條資料,可以進行消費
    return c >= 0; // 新增成功返回true,否則返回false
}

offer(E,long,TimeUnit) 方法

  • 和 ArrayBlockingQueue 一樣,帶阻塞時間引數的 offer() 方法會阻塞一段時間,然後沒結果就返回。

take 方法

  • 這裡和前面一樣,都使用的是 AtomicInteger.getAndDecrement() 方法,這個方法先返回當前值,然後加 1 ,所以後面判斷是判斷之前的情況。
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); // 拿鎖加鎖,保證呼叫take方法的時候只有1個執行緒
    try {
        while (count.get() == 0) { // 如果佇列裡已經沒有元素了
            notEmpty.await(); // 阻塞並掛起當前執行緒
        }
        x = dequeue(); // 刪除頭結點
        c = count.getAndDecrement(); // 元素個數-1
        if (c > 1) // 如果佇列裡還有元素
            notEmpty.signal(); // 在拿鎖的條件物件notEmpty上喚醒正在等待的執行緒,表示佇列裡還有資料,可以再次消費
    } finally {
        takeLock.unlock(); // 釋放拿鎖,讓其他執行緒可以呼叫take方法
    }
    if (c == capacity) // 如果佇列中還可以再插入資料
        signalNotFull(); // 在放鎖的條件物件notFull上喚醒正在等待的1個執行緒,表示佇列裡還能再次新增資料
    return x;
}
  • LinkedBlockingQueue 的 take 方法對於沒資料的情況下會阻塞,poll 方法刪除連結串列頭結點,remove 方法刪除指定的物件。
  • 需要注意的是 remove 方法由於要刪除的資料的位置不確定,需要 2 個鎖同時加鎖。
public boolean remove(Object o) {
    if (o == null) return false;
    fullyLock(); // remove操作要移動的位置不固定,2個鎖都需要加鎖
    try {
        for (Node<E> trail = head, p = trail.next; // 從連結串列頭結點開始遍歷
             p != null;
             trail = p, p = p.next) {
            if (o.equals(p.item)) { // 判斷是否找到物件
                unlink(p, trail); // 修改結點的連結資訊,同時呼叫notFull的signal方法
                return true;
            }
        }
        return false;
    } finally {
        fullyUnlock(); // 2個鎖解鎖
    }
}
  • 隊首元素出隊。
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;    //指向隊首的結點後移
    E x = first.item;
    first.item = null;
    return x;
}
  • LinkedBlockingQueue 比 ArrayBlockingQueue 的優勢就是新增和獲取是兩個不同的鎖,所以併發新增/獲取效率更高些。
    • 陣列元素個數用的是 AtomicInteger 型別的,這樣在新增、獲取時通過判斷陣列元素個數可以感知到併發的獲取/新增操作。
    • 此外連結串列比陣列的優勢。

3.4 PriorityBlockingQueue

  • PriorityBlockingQueue 是基於 陣列 的、支援 優先順序 的、無界 阻塞佇列。
    • 通過使用堆這種資料結構實現將佇列中的元素按照某種排序規則進行排序,從而改變先進先出的佇列順序,提供開發者改變佇列中元素的順序的能力。
      • 堆是一種二叉樹結構,堆的根元素是整個樹的最大值或者最小值(稱為大頂堆或者小頂堆),同時堆的每個子樹都是滿足堆的樹結構。
      • 由於堆的頂部是最大值或者最小值,所以每次從堆獲取資料都是直接獲取堆頂元素,然後再將堆調整成堆結構。
    • 預設情況佇列中的元素按自然排序升序排列,也可以實現元素的 compareTo() 指定元素的排序規則,或者在初始化它時在建構函式中傳遞 Comparator 排序規則。
    • 不能保證同一優先順序元素的順序。

建構函式

  • PriorityBlockingQueue 在初始化時建立指定容量的陣列,預設是 11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private transient Object[] queue;
private transient int size;
public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
}
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}

offer 方法

  • 無界 阻塞佇列。
    • 新增元素時,當陣列中元素大於等於容量時,會呼叫 tryGrow() 擴容。
  • 在擴容時,如果當前佇列中元素個數小於 64 個,陣列容量就乘 2 加 2。否則變成原來的 1.5 倍(容量越大,擴容成本越高,所以容量設定的小一些)。
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);    //拷貝原有資料
    }
}

保證優先順序

  • 沒有設定 Comparator 預設呼叫的是 siftUpComparable()
    • 每從隊尾新增一個元素都會從下往上挨個比較自己和 " 父結點 " 的大小,如果小就交換,否則就停止。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) { // 迴圈比較
        // 尋找k的父元素下標,固定規則
        int parent = (k - 1) >>> 1;    
        Object e = array[parent];
        // 自下而上一般出現在插入元素時呼叫,插入元素是插入到佇列的最後,則需要將該元素調整到合適的位置
        // 即從佇列的最後往上調整堆,直到不小於其父結點為止,相當於冒泡              
        if (key.compareTo((T) e) >= 0)    //比較
            break;
        // 如果當前結點<其父結點,則將其與父結點進行交換,並繼續往上訪問父結點
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

poll 方法

  • 出隊時同樣進行了兩種不同的比較。
public E poll() {
    // size==0佇列為0,直接返回null
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    // 出隊總是將陣列的第一個元素進行出隊,
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        // 同時將佇列的最後一個元素放到第一個位置,然後自上而下調整堆
        siftDown(0, x);
    return result;
}
  • 出隊時同樣進行了兩種不同的比較。
    • 取元素時,在移除第一個元素後,會用堆排序將當前堆再排一次序。
private void siftDownUsingComparator(int k, E x) {
    // 由於堆是一個二叉樹,所以size/2是樹中的最後一個非葉子結點
    // 如果k是葉子結點,那麼其無子結點,則不需要再往下調整堆
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        // 右結點
        int right = child + 1;
        // 找出兩個子結點以及父結點中較小的一個
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        // 如果父結點最小,則無需繼續往下調整堆
        if (comparator.compare(x, (E) c) <= 0)
            break;
        // 否則將父結點與兩個子結點中較小的一個交換,然後往下繼續調整
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}
  • PriorityBlockingQueue 是無界、有優先順序的佇列。
    • 可以擴容,在新增、刪除元素後都會進行排序。

3.5 DelayQueue

  • DelayQueue 是一個支援 延時 獲取元素的、無界 阻塞佇列。
  • 佇列使用 PriorityQueue 實現,佇列中的元素必須實現 Delayed 介面。
    • 實現 Delayed 的類也需要實現 Comparable 介面,即實現 compareTo() 方法,保證集合中元素的順序和 getDelay() 一致。
public interface Delayed extends Comparable<Delayed> {
    //返回當前物件的剩餘執行時間
    long getDelay(TimeUnit unit);
}
  • 建立元素時可以指定多久才能從佇列中獲取當前元素。

3.5.1 DelayQueue 的關鍵屬性

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader;

/**
 * Condition signalled when a newer element becomes available
 * at the head of the queue or a new thread may need to
 * become leader.
 */
private final Condition available = lock.newCondition();
屬性 說明
ReentrantLock lock 重入鎖。
PriorityQueue q 無界的、優先順序佇列。
Thread leader Leader-Follower 模型中的 leader
Condition available 隊首有新元素可用或者有新執行緒成為 leader 時觸發的 condition。

PriorityQueue

  • PriorityQueue 是一個用 陣列 實現的,基於二叉堆(元素[n] 的子孩子是 元素[2n+1] 和元素[2(n+1)] )資料結構的集合。
  • 在新增元素時如果超出限制也會擴容,是 無界 的。
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;
}

Leader-Follower 模型

5714666-8472c1105bb3416e.png
Leader-Follower 模型
  • 這種模型中所有執行緒分為三種身份 Leader、Follower、Proccesser。
  • 基本原則是最多隻有一個 Leader。所有 Follower 都在等待成為 Leader。
  • 佇列中的 leader 是一個等待獲取佇列頭部元素的執行緒。
    • 如果 leader 不等於空,表示已經有執行緒在等待獲取佇列的頭元素,然後使用 await() 方法讓當前執行緒等待訊號。
    • 如果 leader 為空,則把當前執行緒設定成 leader,並使用 awaitNanos() 方法讓當前執行緒等待接收訊號或等待 delay 時間。
  • 這種方法可以增強 CPU 快取記憶體相似性,及消除動態記憶體分配和執行緒間的資料交換。這種模式是為了最小化任務等待時間,當一個執行緒成為 leader 後,它只需要等待下一個可執行任務的出現,而其他執行緒要無限制地等待。

實現 Delayed 介面

  • DelayQueue 的元素必須實現 Delayed 介面,實現 Delayed 介面大概有三步。
    • 建構函式中初始化基本資料,比如執行時間等資料。
    • 實現 getDelay() 方法,返回當前元素還需要延時多久執行。
    • 實現 compareTo() 方法,指定不同元素如何比較誰先執行。

延時阻塞佇列的實現

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 <= 0L)    //比較元素延時時間是否到達
                    return q.poll();    //如果是就移除並返回
                first = null; // don't retain ref while waiting
                if (leader != null)    //如果有 leader 執行緒,依然阻塞等待
                    available.await();
                else {        //如果沒有 leader 執行緒,指定當前執行緒,然後等待任務的待執行時間
                    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();
    }
}

//PriorityQueue.peek()
public E peek() {
    return (size == 0) ? null : (E) queue[0];
}
  • 取元素時,會根據元素的延時執行時間是否為 0 進行判斷,如果延時執行時間已經沒有了,就直接返回,否則需要等待執行時間到達後再返回。

3.6 SynchronousQueue

  • SynchronousQueue 支援公平訪問佇列,根據建構函式的引數不同,有兩種實現方式:TransferQueue 和 TransferStack,預設情況下是 false。
private transient volatile Transferer<E> transferer;

public SynchronousQueue() {
    this(false);
}

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
  • SynchronousQueue 是一個 不儲存元素 的阻塞佇列。
    • 這裡的 " 不儲存元素 " 指的是,SynchronousQueue 容量為 0,每新增一個元素必須等待被取走後才能繼續新增元素

put 方法

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {    
        Thread.interrupted();
        throw new InterruptedException();
    }
}
  • 新增是呼叫的 transferer.transfer(),如果返回 null 就呼叫 Thread.interrupted() 將中斷標誌位復位(設為 false),然後丟擲異常。
/**
 * Puts or takes an item.
 */
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; 
    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
            }
        }
    }
}
  • 主要步驟。
    1. 棧是空的或者棧頂元素的模式和當前要進行的操作一致。
      • 將結點推到堆疊上並等待匹配。
      • 等待引數中的時間後返回。
      • 如果取消就返回 null。
    2. 如果棧不為空且棧頂元素模式與當前要進行的操作不一致,如果這個元素的模式是相反的模式。
      • 嘗試將棧中一個模式匹配要求的結點推到堆疊上,與相應的等待結點匹配並返回。
    3. 如果棧頂已經擁有另一個模式匹配的結點。
      • 通過執行 POP 操作來找到匹配的元素,然後繼續。
  • 簡單概括就是一個新增操作後必須等待一個獲取操作才可以繼續新增。

結論

  • LinkedBlockingQueue 效能表現遠超 ArrayBlcokingQueue,不管執行緒多少,不管 Queue 長短,LinkedBlockingQueue 都勝過 ArrayBlockingQueue。
  • SynchronousQueue 表現很穩定,而且在 20 個執行緒之內不管 Queue 長短,SynchronousQueue 效能表現是最好的,(其實 SynchronousQueue 跟 Queue 長短沒有關係),如果 Queue 的 capability 只能是 1,那麼毫無疑問選擇 SynchronousQueue,這也是設計 SynchronousQueue 的目的。
  • 當超過 1000 個執行緒時,SynchronousQueue 效能就直線下降,只有最高峰的一半左右,而且當 Queue 大於 30 時,LinkedBlockingQueue 效能就超過 SynchronousQueue。
  • 相較於其他佇列有快取的作用,SynchronousQueue 適用於單執行緒同步傳遞性場景,比如:消費者沒拿走當前的產品,生產者是不能再給產品,這樣可以控制生產者生產的速率和消費者一致。

3.7 LinkedTransferQueue

  • LinkedTransferQueue 實現了 TransferQueue 介面, 是一個由 連結串列 組成的、無界 阻塞佇列。
public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {...}

TransferQueue

  • TransferQueue 也是一種阻塞佇列,用於生產者需要等待消費者消費事件的場景,與 SynchronousQueue 有相似之處。
public interface TransferQueue<E> extends BlockingQueue<E> {
    //儘可能快地轉移元素給一個等待的消費者
    //如果在這之前有其他執行緒呼叫了 taked() 或者 poll(long,TimeUnit) 方法,就返回 true
    //否則返回 false
    boolean tryTransfer(E e);

    //轉移元素給一個消費者,在有的情況下會等待直到被取走
    void transfer(E e) throws InterruptedException;

    //在 timeout 時間內將元素轉移給一個消費者,如果這段時間內傳遞出去了就返回 true
    //否則返回 false
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //如果至少有一個等待的消費者,就返回 true
    boolean hasWaitingConsumer();

    //返回等待獲取元素的消費者個數
    //這個值用於監控
    int getWaitingConsumerCount();
}
  • 相對於其他阻塞佇列,LinkedTransferQueue 多了兩個關鍵地方法 tryTransfer()transfer()

transfer 方法

  • transfer() 方法的作用是如果有等待接收元素的消費者執行緒,直接把生產者傳入的元素 transfer 給消費者。
  • 如果沒有消費者執行緒,transfer() 會將元素存放到佇列尾部,並等待元素被消費者取走才返回。
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
  • awaitMatch() 方法的作用是,CPU 自旋等待消費者取走元素,為了避免長時間消耗 CPU,在自旋一定次數後會呼叫 Thread.yield() 暫停當前正在執行的執行緒,改為執行其他執行緒。

tryTransfer 方法

  • tryTransfer() 的作用是試探生產者傳入的元素是否能直接傳遞給消費者。
    • 如果有等待接收的消費者,返回 true。
    • 沒有則返回 false。
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();
}
  • transfer() 必須等到消費者取出元素才返回不同的是,tryTransfer() 無論是否有消費者接收都會立即返回。
  • 對於帶有時間限制的 tryTransfer(E,long,TimeUnit) 方法,試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回 false,如果在超時時間內消費了元素,則返回 true。

3.8 LinkedBlockingDeque

  • LinkedBlockingDeque 是一個由 連結串列 組成的、雙向 阻塞佇列。

關鍵屬性

static final class Node<E> {
    E item;
    Node<E> prev;
    Node<E> next;
    Node(E x) {
        item = x;
    }
}

transient Node<E> first;
transient Node<E> last;
private transient int count;
private final int capacity;
final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
  • LinkedBlockingDeque 中持有佇列首部和尾部結點,每個結點也是雙向的。
    • 雙向的作用是可以從佇列兩端插入和移除元素。多了一個操作佇列的方向,在多執行緒同時入隊時,可以減少一半的競爭
    • 除了 remove(Object) 等移除操作,LinkedBlockingDeque 的大多數操作的時間複雜度都是 O(n)

4. 阻塞佇列的特點

  • 阻塞佇列使用最經典的場景就是 socket 客戶端資料的讀取和解析,讀取資料的執行緒不斷將資料放入佇列,然後解析執行緒不斷從佇列取資料解析。
  • 還有其他類似的場景,只要符合 生產者-消費者模型 的都可以使用阻塞佇列。

ArrayBlockingQueue

  • 環形陣列 實現的、有界 的佇列,一旦建立後,容量不可變
  • 基於陣列,在新增刪除上 效能不如連結串列

LinkedBlockingQueue

  • 基於 連結串列有界 阻塞佇列。
  • 新增和獲取是 兩個不同的鎖,所以 併發新增/獲取效率更高些
  • Executors.newFixedThreadPool() 使用了這個佇列。

PriorityBlockingQueue

  • 基於 陣列 的、支援 優先順序 的、無界 阻塞佇列。
  • 使用 自然排序或者定製排序 指定排序規則。
  • 新增元素時,當陣列中元素 大於等於容量 時,會 擴容(當前佇列中元素個數小於 64 個,陣列容量 乘 2 加 2,否則變成原來的 1.5 倍),拷貝陣列。

DelayQueue

  • 支援 延時獲取元素 的、無界 阻塞佇列。
  • 新增元素時如果超出限制也會 擴容
  • 採用 Leader-Follower 模型

SynchronousQueue

  • 容量為 0
  • 一個新增操作後 必須等待 一個獲取操作才可以繼續新增。
  • 吞吐量高於 LinkedBlockingQueue 和 ArrayBlockingQueue。

LinkedTransferQueue

  • 連結串列 組成的、無界 阻塞佇列。
  • 實現了 TransferQueue 介面。
  • CPU 自旋 等待消費者取走元素,自旋一定次數後結束。

LinkedBlockingDeque

  • 雙向連結串列 組成的、雙向 阻塞佇列。
  • 可以從佇列 兩端 插入和移除元素。
  • 多了一個操作佇列的方向,在多執行緒同時入隊時,可以 減少一半 的競爭。

參考資料

https://blog.csdn.net/u011240877/article/details/73612930#1arrayblockingqueue
https://blog.csdn.net/fuyuwei2015/article/details/72716753
https://blog.csdn.net/tonywu1992/article/details/83419448

相關文章