阻塞佇列一——java中的阻塞佇列

bmilk發表於2020-06-11

目錄

  • 阻塞佇列簡介:介紹阻塞佇列的特性與應用場景
  • java中的阻塞佇列:介紹java中實現的供開發者使用的阻塞佇列
  • BlockQueue中方法:介紹阻塞佇列的API介面
  • 阻塞佇列的實現原理:具體的例子說明阻塞佇列的實現原理
  • 總結

阻塞佇列簡介

阻塞佇列(BlockingQueue)首先是一個支援先進先出的佇列,與普通的佇列完全相同;
其次是一個支援阻塞操作的佇列,即:

  • 當佇列滿時,會阻塞執行插入操作的執行緒,直到佇列不滿。
  • 當佇列為空時,會阻塞執行獲取操作的執行緒,直到佇列不為空。

阻塞佇列用在多執行緒的場景下,因此阻塞佇列使用了鎖機制來保證同步,這裡使用的可重入鎖;
而對於阻塞與喚醒機制則有與鎖繫結的Condition實現

應用場景:生產者消費者模式

java中的阻塞佇列

java中的阻塞佇列根據容量可以分為有界佇列和無界佇列:

  • 有界佇列:佇列中只能儲存有限個元素,超出後存放元素執行緒會被阻塞或者失敗。
  • 無界佇列:佇列中可以儲存無限個元素。

java8中提供了7種阻塞佇列阻塞佇列供開發者使用,如下表:

類名 描述
ArrayBlockingQueue 一個由陣列結構組成的有界阻塞佇列
LinkedBlockingQueue 由連結串列結構組成的有界阻塞佇列(預設大小Integer.MAX_VALUE)
PriorityBlockingQueue 支援優先順序排序的無界阻塞佇列
DelayQueue 使用優先順序佇列實現的延遲無界阻塞佇列
SynchronousQueue 不儲存元素的阻塞佇列,即單個元素的佇列
LinkedTransferQueue 由連結串列結構組成的無界阻塞佇列
LinkedBlockingDeque 由連結串列結構組成的雙向阻塞佇列

另外還有一個在ScheduledThreadPoolExecutor中實現的DelayedWorkQueue阻塞佇列,
但這個阻塞佇列開發者不能使用。它們之間的UML類圖如下圖:

BlockingQueue介面是阻塞佇列對外的訪問介面,所有的阻塞佇列都實現了BlockQueue中的方法

BlockQueue中方法

作為一個佇列的核心方法就是入隊和出隊。由於存在阻塞策略,BlockQueue將出隊入隊的情況分為了四組,每組提供不同的方法:

  • 丟擲異常:當佇列滿時,如果再往佇列中插入元素,則丟擲IllegalStateException異常;
    當佇列為空時,從佇列中獲取元素則丟擲NoSuchElementException異常。

  • 返回特定值(布林值):當佇列滿時,如果再往佇列中插入元素,則返回false;當佇列為空時,從佇列中獲取元素則返回null。

  • 一直阻塞:當佇列滿時,如果再往佇列中插入元素,阻塞當前執行緒直到佇列中至少一個被移除或者響應中斷退出;
    當佇列為空時,則阻塞當前執行緒直到至少一個元素元素入隊或者響應中斷退出。

  • 超時退出:當佇列滿時,如果再往佇列中插入元素,阻塞當前執行緒直到佇列中至少一個被移除或者達到指定的等待時間退出或者響應中斷退出;
    當佇列為空時,則阻塞當前執行緒直到至少一個元素元素入隊或者達到指定的等待時間退出或者響應中斷退出。

對於每種情況BlockingQueue提供的方法如下表:

方法\處理方式 丟擲異常 返回特定值(布林值) 一直阻塞 超時退出
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time.unit)
檢查 element() peek() 不可用 不可用

上述方法一般用於生產者-消費者模型中,是其中的生產和消費操作佇列的核心方法。
除了這些方法,BlockingQueue還提供了一些其他的方法如下表:

方法名稱 描述
remove(Object o) 從佇列中移除一個指定值
size() 獲取佇列中元素的個數
contains(Object o) 判斷佇列是否包含指定的元素,但是這個元素在這次判斷完可能就會被消費
drainTo(Collection<? super E> c) 將佇列中元素放在給定的集合中,並返回新增的元素個數
drainTo(Collection<? super E> c, int maxElements) 將佇列中元素取maxElements(不超過佇列中元素個數)個放在給定的集合中,並返回新增的元素個數
remainingCapacity() 計算佇列中還可以存放的元素個數
toArray() 以objetc陣列的形式獲取佇列中所有的元素
toArray(T[] a) 以給定型別陣列的方式獲取佇列中所有的元素
clear() 清空佇列,危險的操作

阻塞佇列的實現原理

阻塞佇列的實現依靠通知模式實現:當生產者向滿了的佇列中新增元素時,會阻塞住生產者,
直到消費者消費了一個佇列中的元素後會通知消費者佇列可用,此時再由生產者向佇列中新增元素。反之亦然。

阻塞佇列的阻塞喚醒依靠Condition——條件佇列來實現。

ArrayBlockingQueue為例說明:

ArrayBlockingQueue的定義:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
   
    /** The queued items */
    //以陣列的結構儲存佇列的元素,採用的是迴圈陣列
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    //佇列的隊頭索引
    int takeIndex;

    /** items index for next put, offer, or add */
    //佇列的隊尾索引
    int putIndex;

    /** Number of elements in the queue */
    //佇列中元素的個數
    int count;

    /** Main lock guarding all access */
    //對於ArrayBlockingQueue所有的操作都需要加鎖,
    final ReentrantLock lock;

    /** Condition for waiting takes */
    //條件佇列,當佇列為空時阻塞消費者並在生產者生產後喚醒消費者
    private final Condition notEmpty;

    /** Condition for waiting puts */
    //條件佇列,當佇列滿時阻塞生產者,並在消費者消費佇列後喚醒生產者
    private final Condition notFull;
}

根據類的定義欄位可以看到,有兩個Condition條件佇列,猜測以下過程

  • 當佇列為空,消費者試圖消費時應該呼叫notEmpty.await()方法阻塞,並在生產者生產後呼叫notEmpty.single()方法
  • 當佇列已滿,生產者試圖放入元素應呼叫notFull.await()方法阻塞,並在消費者消費佇列後呼叫notFull.single()方法

向佇列中新增元素put()方法的新增過程。

    /**
    * 向佇列中新增元素
    * 當佇列已滿時需要阻塞當前執行緒
    * 放入元素後喚醒因佇列為空阻塞的消費者
    */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //當佇列已滿時需要notFull.await()阻塞當前執行緒
            //offer(e,time,unit)方法就是阻塞的時候加了超時設定
            while (count == items.length)
                notFull.await();
            //放入元素的過程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    /**enqueue實際新增元素的方法*/
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //如果條件佇列中存在等待的執行緒
        //喚醒
        notEmpty.signal();
    }

從佇列中獲取元素take()方法的獲取過程。

    /**
    * 從佇列中獲取元素
    * 當佇列已空時阻塞當前執行緒
    * 從佇列中消費元素後喚醒等待的生產執行緒
    */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //佇列為空需要阻塞當前執行緒
            while (count == 0)
                notEmpty.await();
            //獲取元素的過程
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    /**dequeue實際消費元素的方法*/
    private E dequeue() {
       // assert lock.getHoldCount() == 1;
       // assert items[takeIndex] != null;
       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;
    }

總結

阻塞佇列提供了不同於普通佇列的增加、刪除元素的方法,核心在與佇列滿時阻塞生產者和佇列空時阻塞消費者。
這一阻塞過程依靠與鎖繫結的Condition物件實現。Condition介面的實現在AQS中實現,具體的實現類是
ConditionObject

相關文章