阻塞佇列 BlockingQueue

eluanshi12發表於2018-12-14

執行緒安全的佇列訪問

主要應用場景:生產者消費者模型,是執行緒安全的
BlockingQueue執行緒安全
阻塞情況:

  • 當佇列滿了進行入隊操作
  • 當佇列空了的時候進行出佇列操作

4種處理方式

插入和移除操作的4中處理方式

  • (Throws Exceptions) 丟擲異常:當佇列滿時,如果再往佇列裡插入元素,會丟擲IllegalStateException(“Queue full”)異常。當佇列空時,從佇列裡獲取元素會丟擲NoSuchElementException異常。
  • (Special Value)返回特殊值:當往佇列插入元素時,會返回元素是否插入成功,成功返回true。如果是移除方法,則是從佇列裡取出一個元素,如果沒有則返回null。
  • (Blocks)一直阻塞:當阻塞佇列滿時,如果生產者執行緒往佇列裡put元素,佇列會一直阻塞生產者執行緒,直到佇列可用或者響應中斷退出。當佇列空時,如果消費者執行緒從佇列裡take元素,佇列會阻塞住消費者執行緒,直到佇列不為空。
  • (Times Out)超時退出:當阻塞佇列滿時,如果生產者執行緒往佇列裡插入元素,佇列會阻塞生產者執行緒一段時間,如果超過了指定的時間,生產者執行緒就會退出。

注意: 如果是無界阻塞佇列,佇列不可能會出現滿的情況,所以使用put或offer方法永遠不會被阻塞,而且使用offer方法時,該方法永遠返回true。

實現類:

Java併發包中的7個阻塞佇列:

佇列 有界性 特點
ArrayBlockingQueue bounded(有界) 加鎖 陣列 FIFO
LinkedBlockingQueue 指定-有界、不指定-無界 加鎖 連結串列 FIFO
PriorityBlockingQueue 無界 加鎖 優先順序排序(預設升序)
DelayQueue 無界 加鎖 延時獲取 使用PriorityQueue實現
SynchronousQueue bounded 加鎖 不儲存元素
LinkedTransferQueue 無界 加鎖 連結串列 (傳輸)
LinkedBlockingDeque 無界 無鎖 連結串列 雙向

ArrayBlockingQueue

初始化時指定容量大小,一旦指定大小就不能再變。採用FIFO方式儲存元素。

  • 預設情況下不保證執行緒公平的訪問佇列。
  • 公平訪問佇列是指阻塞的執行緒,可以按照阻塞的先後順序訪問佇列,即先阻塞執行緒先訪問佇列。
  • 非公平性是對先等待的執行緒是非公平的,當佇列可用時,阻塞的執行緒都可以爭奪訪問佇列的資格,有可能先阻塞的執行緒最後才訪問 佇列。
  • 為了保證公平性,通常會降低吞吐量。

使用以下程式碼建立一個公平的阻塞佇列。

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);//初始化ReentrantLock重入鎖,出隊入隊擁有這同一個鎖 
  notEmpty = lock.newCondition;//初始化非空等待佇列
  notFull = lock.newCondition;//初始化非滿等待佇列 
} 

LinkedBlockingQueue

大小配置可選,如果初始化時指定了大小,那麼它就是有邊界的。不指定就無邊界(最大整型值)。內部實現是連結串列,採用FIFO形式儲存資料。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);//不指定大小,無邊界採用預設值,最大整型值
}

PriorityBlockingQueue

帶優先順序的阻塞佇列(預設升序)。無邊界佇列,允許插入null 。插入的物件必須實現Comparator介面, 對Comparable介面的實現來指定佇列優先順序的排序規則。我們可以從PriorityBlockingQueue中獲取一個迭代器,但這個迭代器並不保證能按照優先順序的順序進行迭代

public boolean add(E e) {//新增方法
    return offer(e);
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;//必須實現Comparator介面
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

SynchronusQueue

不儲存元素,非常適合傳遞性場景,吞吐量較高。每一個put操作必須等待一個take操作,否則不能繼續新增元素。

它支援公平訪問佇列。預設情況下執行緒採用非公平性策略訪問佇列

//建立公平性訪問 fair=true,則等待的執行緒會採用先進先出的順序訪問佇列。
public SynchronousQueue(boolean fair) {
	transferer = fair?new TransferQueue() : new TransferStack();
}

LinkedTransferQueue

相對於其他阻塞佇列,LinkedTransferQueue多了tryTransfer和transfer方法。

(1)transfer方法

  • 如果當前有消費者正在等待接收元素(消費者使用take()方法或帶時間限制的poll()方法時),transfer方法可以把生產者傳入的元素立刻transfer(傳輸)給消費者。
  • 如果沒有消費者在等待接收元素,transfer方法會將元素存放在佇列的tail節點,並等到該元素被消費者消費了才返回
//試圖把存放當前元素的s節點作為tail節點
Node pred = tryAppend(s, haveData);
//CPU自旋等待消費者消費元素。
//因為自旋會消耗CPU,所以自旋一定的次數後使用Thread.yield()方法來暫停當前正在執行的執行緒,並執行其他執行緒。
return awaitMatch(s, pred, e, (how == TIMED), nanos);

(2)tryTransfer方法

  • tryTransfer方法是用來試探生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回false。
  • 對於帶有時間限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回false,如果在時限內消費了元素,則返回true。

(3)區別
tryTransfer方法無論消費者是否接收,方法立即返回,而transfer方法是必須等到消費者消費了才返回。

LinkedBlockingDeque

LinkedBlockingDeque是一個由連結串列結構組成的雙向阻塞佇列(可以從佇列的兩端插入和移出元素,減少了一半的競爭)。
相比其他的阻塞佇列,LinkedBlockingDeque多了
addFirst、offerFirst、peekFirst
addLast、offerLast、peekLast 等方法
以First單詞結尾的方法,表示插入、獲取(peek)或移除雙端佇列的第一個元素。
以Last單詞結尾的方法,表示插入、獲取(peek)或移除雙端佇列的最後一個元素。

插入方法add等同於addLast
移除方法remove等效於removeFirst,但是take方法卻等同於takeFirst。
使用時還是用帶有First和Last字尾的方法更清楚。

在初始化LinkedBlockingDeque時可以設定容量防止其過度膨脹。另外,雙向阻塞佇列可以運用在“工作竊取”模式中。

阻塞佇列的實現原理

使用通知模式實現:當生產者往滿的佇列裡新增元素時會阻塞住生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。(JDK原始碼ArrayBlockingQueue使用了Condition來實現)

當往佇列裡插入一個元素時,如果佇列不可用,那麼阻塞生產者主要通過LockSupport.park(this)來實現。

參考:
慕課網實戰·高併發探索(十三):併發容器J.U.C – 元件FutureTask、ForkJoin、BlockingQueue
阻塞佇列 BlockingQueue
BlockingQueue深入解析
《java併發程式設計的藝術》

相關文章