認識一下阻塞隊列

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。當隊列滿時,存儲元素的線程會等待隊列可用。阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素到容器,而消費者也只從容器裏拿元素。

四種處理方法

1拋出異常

是指當阻塞隊列已滿時候,如果再往隊列裏插入元素,會拋出IllegalStateException異常。當隊列爲空時,從隊列裏獲取元素時會拋出NoSuchElementException異常 。

2返回特殊值

插入方法會返回是否成功,成功則返回true。移除方法,則是從隊列裏拿出一個元素,如果沒有則返回null。

3一直阻塞

當阻塞隊列已滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到拿到數據,或者響應中斷退出。當隊列爲空時,如果消費者線程從隊列裏take元素,隊列也會阻塞消費者線程,直到隊列可用。

4超時退出

當阻塞隊列已滿時,隊列會阻塞生產者線程一段時間,如果超過一定的時間,生產者線程就會退出。

 

ArrayBlockingQueue

ArrayBlockingQueue 是一個有界的阻塞隊列,其內部實現是將對象放到一個數組裏。它不能夠存儲無限多數量的元素。一旦創建,就不能更改容量。這個類支持一個可選的公平性策略,用於排序等待的生產者和使用者線程。默認情況下,不設置順序。但是,公平性設置爲true的隊列按FIFO順序授予線程訪問權。公平性通常會降低吞吐量,但會減少可變性並避免飢餓。

DelayQueue

DelayQueue是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。

LinkedBlockingQueue

LinkedBlockingQueue 內部以一個鏈式結構(鏈接節點)對其元素進行存儲。如果沒有定義上限,將使用 Integer.MAX_VALUE 作爲上限。

PriorityBlockingQueue

PriorityBlockingQueue 是一個支持優先級的無界隊列。它使用了和類 java.util.PriorityQueue 一樣的排序規則。無法向隊列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必須實現 java.lang.Comparable 接口。因此該隊列中元素的排序就取決於自己的 Comparable 實現。

SynchronousQueue

SynchronousQueue 是一個特殊的隊列,它的內部同時只能夠容納單個元素。如果該隊列已有一個元素的話,試圖向隊列中插入一個新元素的線程將會阻塞,直到另一個線程將該元素從隊列中抽走。同樣,如果該隊列爲空,試圖向隊列中抽取一個元素的線程將會阻塞,直到另一個線程向隊列中插入了一條新的元素。據此,把這個類稱作一個隊列顯然是誇大其詞了。它更多像是一個匯合點。

LinkedTransferQueue

LinkedTransferQueue是基於鏈接節點的無限阻塞隊列。此隊列遵循FIFO(先進先出)。隊列的head是某個生產者在隊列中停留時間最長的元素。隊列的tail是某個生產者在隊列中停留的最短時間的元素。

LinkedBlockingDeque

LinkedBlockingDeque是基於鏈接節點的可選有界的阻塞隊列,可選的容量可用於防止過度擴展。如果未指定,則容量等於Integer.MAX_VALUE。鏈接節點在每次插入時都是動態創建的,除非這會使deque超過容量。

public static void main(String[] args) {
    ArrayBlockingQueue<Integer> abq =
            new ArrayBlockingQueue<Integer>(3);

    new Thread(() -> {
        while (true){
            try {
                abq.put(1);
                System.out.println(Thread.currentThread().getName()
                        + "放入一個數據");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    new Thread(() -> {
        while (true){
            try {
                Integer take = abq.take();
                System.out.println(Thread.currentThread().getName()
                        + "取出一個數據 " + take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

輸出結果:

Thread-0放入一個數據
Thread-0放入一個數據
Thread-0放入一個數據
Thread-1取出一個數據 1
Thread-1取出一個數據 1
Thread-1取出一個數據 1
Thread-1取出一個數據 1
Thread-0放入一個數據
Thread-0放入一個數據
Thread-1取出一個數據 1
Thread-1取出一個數據 1
Thread-0放入一個數據
Thread-0放入一個數據
Thread-1取出一個數據 1
Thread-0放入一個數據
...

看到這個輸出結果,你可能會有所疑問,好像放入和取出並不配對,這是因爲put、take和System.out.println並非原子操作。

 

阻塞隊列原理

Java中使用通知模式實現阻塞隊列的通訊。就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,並通知消費者可以取出元素了,當消費者消費了一個隊列中的元素後,會通知生產者可以放入元素了。在ArrayBlockingQueue中 使用了 Condition 來實現。

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** 隊列元素 */
    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 */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;
    ...
}

put(E e)

public void put(E e) throws InterruptedException {
      checkNotNull(e);
      final ReentrantLock lock = this.lock;
      lock.lockInterruptibly();
      try {
      // 隊列已經放滿了
          while (count == items.length)
              notFull.await();
        // 沒放滿,則放入隊列
          enqueue(e);
      } finally {
          lock.unlock();
      }
  }

當隊列已經放滿的時候,就會調用Condition的await方法就會阻塞當前線程,讓生產者等待。如果沒放滿,則調用enqueue方法加入隊列並釋放消費者線程。

enqueue(e)

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    // 當隊列放滿的時候,putIndex重新從 0 開始
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    // 喚醒消費者線程
    notEmpty.signal();
}

putIndex和takeIndex分別對應放入和取出的遊標.


ArrayBlockingQueue<Integer> abq =
            new ArrayBlockingQueue<Integer>(3);

表示item數組的長度爲3,當放入第一個元素的時候,putIndex爲0,放入第二個元素的時候,putIndex爲1,以此類推,當我們放入第三個的時候,putIndex爲2,那麼++putIndex爲3,此時數組已滿,則將putIndex歸0,因爲我們調用take的時候,takeIndex也是從0開始的。

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;
}

當取出一個元素時,同時喚醒生產者線程。

更多精彩內容請關注微信公衆號:

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