java 中的阻塞隊列

一. 什麼是隊列

隊列是一種特殊的線性表, 特殊之處在於它只允許在表的前端 (front) 進行刪除操作, 而在表的後端 (rear) 進行插入操作. 和棧一樣, 隊列是一種操作受限制的線性表. 進行插入操作的端稱爲隊尾, 進行刪除操作的端稱爲對頭.

在隊列中插入一個隊列元素稱爲入隊, 從隊列中刪除一個隊列元素稱爲出隊. 因爲隊列只允許在一端插入, 在另一端刪除, 所以只有最早進入隊列的元素才能最先從隊列中刪除. 故隊列又稱爲先進先出 (FIFO - first in first out) 線性表.


 

二. 什麼是阻塞隊列

簡單來說就是支持阻塞插入和阻塞移除的隊列就可被稱爲阻塞隊列.

  • 阻塞插入
    意思是當隊列滿的時候, 隊列會阻塞插入元素的線程, 直到隊列有空缺. 纔可以繼續插入元素.

  • 阻塞移除
    當隊列爲空的時候, 獲取隊列元素的線程會一直等待隊列變爲非空.

阻塞隊列常用於生產者和消費者的場景, 生產者是向隊列裏添加元素的線程, 消費者是從隊列裏取元素的線程. 阻塞隊列就是生產者用來存放元素, 消費者用來獲取元素的容器.

在線程世界裏, 生產者就是生產數據的線程, 消費者就是消費數據的線程. 在多線程開發中, 如果生產者處理速度很快, 而消費者處理速度很慢, 那麼生產者就必須等待消費者處理完, 才能繼續生產數據. 同樣道理, 如果消費者的處理能力大於生產者, 那麼消費者也就必須等待生產者.

爲了解決這種生產消費能力不均衡的的問題, 便有了生產者和消費者模式. 生產者和消費者通過一個容器來解決生產者和消費者的強耦合問題. 生產者和消費者彼此之間不直接通信, 而是通過阻塞隊列來進行通信, 所以生產者生產完數據之後不用等待消費者處理, 直接丟給阻塞隊列. 消費者則不需要找生產者要數據, 而是直接從阻塞隊列裏取. 阻塞隊列就相當於一個緩衝區, 平衡了生產者和消費者的處理能力.
 

三. 常用阻塞隊列

JDK 爲我們提供了一個繼承自 Queue 接口的 BlockingQueue 接口 , 常用的阻塞隊列幾乎都實現了此接口. 這些方法並非都是阻塞類的方法, 還包含了非阻塞類的方法. 這兩種方法基本都是成對出現的.例如插入和獲取.

方法/處理方式 拋出異常 返回特殊值 一直阻塞 超時退出
插入方法 add(E) offer(E) put(E) offer(E, long, TimeUnit)
移除方法 remove() poll() take() poll(long, TimeUnit)
檢查方法 element() peek()

真正體現了阻塞的兩個方法就是 put(E) / take()

  • 拋出異常
    當隊列滿的時候, 如果再向隊列中插入元素, 會拋出默認的 IllegalStateException 異常.
    當隊列爲空時候, 如果再從隊列中獲取元素, 會拋出 NoSuchElementException 異常

  • 返回特殊值
    每次對隊列執行插入操作, 會返回元素是否插入成功, 成功則返回 true.
    如果是獲取操作, 則是從隊列中獲取一個元素, 沒有這個元素的話, 返回值爲 null.

  • 一直阻塞
    當隊列滿的時候, 如果生產者線程繼續向隊列中 put 元素, 隊列將會一直阻塞生產者線程, 直到隊列可用或者響應中斷退出.
    當隊列爲 null 的時候, 如果消費者線程從隊列中 take 元素, 隊列會阻塞住消費者線程, 直到隊列不爲 null.

  • 超時退出
    當阻塞隊列滿時, 如果生產者線程繼續向隊列中插入元素, 隊列會阻塞生產者線程一段時間, 如果超過了這個指定的時間, 生產者線程就會退出.

 

JDK 中實現了 BlockingQueue 接口的阻塞隊列如下圖所示

  • ArrayBlockingQueue: 一個由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue: 一個由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue: 一個支持優先級排序的無界阻塞隊列。
  • DelayQueue: 一個使用優先級隊列實現的無界阻塞隊列。
  • SynchronousQueue: 一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue: 一個由鏈表結構組成的無界阻塞隊列。
  • LinkedBlockingDeque: 一個由鏈表結構組成的雙向阻塞隊列。

以上的阻塞隊列都實現了 BlockingQueue 接口, 同時也都是線程安全的. (其中 LinkedBlockingDeque 實現了 BlockingDeque 接口. LinkedTransferQueue 實現了 TransferQueue 接口).

有界隊列與無界隊列的概念
  • 有界隊列就是長度有限, 滿了以後生產者就會阻塞. 無界隊列就是隊列中能放無數的東西, 而不會因爲隊列長度限制被阻塞. 當然還是會有限制的, 限制來源於系統資源的限制, 如果處理不及時, 導致隊列越來越大超出一定的限制使內存超限, 會被操作系統或者 JVM 直接殺掉.

  • 其實無界隊列也會阻塞, 因爲阻塞不僅體現在生產者放入元素的時候會阻塞, 消費者拿元素的時候, 如果沒有隊列中沒有元素, 同樣也會阻塞.

1. ArrayBlockingQueue
  • 是一個用數組實現的有界阻塞隊列. 此隊列按照先進先出(FIFO) 的原則對元素進行排序. 默認情況下不保證線程公平的訪問隊列, 所謂公平訪問隊列是指阻塞的線程, 可以按照阻塞的先後順序訪問隊列, 即先阻塞線程先訪問隊列. 非公平性是對先等待的線程是非公平的, 當隊列可用時, 阻塞的線程都可以爭奪訪問隊列的資格, 有可能先阻塞的線程最後才訪問隊列. 初始化時有參數可以設置.
2. LinkedBlockingQueue
  • 是一個用鏈表實現的有界阻塞隊列, 此隊列的默認和最大長度爲 Integer.MAX_VALUE. 此隊列按照先進先出的原則對元素進行排序.
3. PriorityBlockingQueue
  • 是一個支持優先級的無界阻塞隊列. 默認情況下元素纔去自然順序升序排列. 也可以自定義類實現 compareTo() 方法來指定元素的排序規則, 或者初始化 PriorityBlockingQueue 時, 指定構造參數 Comparator 來對元素進行排序. 需要注意的是不能保證同優先級元素的順序.
4. DelayQueue
  • 是一個支持延遲獲取元素的無界阻塞隊列. 隊列使用 PriorityQueue 來實現. 隊列中的元素必須實現 Delayed 接口. 在創建元素時可以指定多久才能從隊列中獲取當前元素. 只有在延遲期滿的時候才能從隊列中提取元素.
5. SynchronousQueue
  • 是一個不存儲元素的阻塞隊列. 每一個 put 操作必須等待一個 take 操作. 否則不能繼續添加元素. 可以理解爲 SynchronousQueue 負責把生產者線程處理的數據直接傳遞給消費者線程. 隊列本身不存儲任何元素, 適合傳遞類的場景.

  • SynchronousQueue 的吞吐量高於 ArrayBlockingQueueLinkedBlockingQueue

6. LinkedTransferQueue
  • 多了 transfertryTransfer 方法

  • transfer: 如果當前有消費者線程正在等待接收數據 (消費者線程使用 take() 方法或帶時間限制的 poll() 方法時) , transfer 方法可以把生產者傳入的元素立刻傳遞給消費者線程. 如果沒有消費者在等待接收元素, transfer 方法會將元素存放在隊列的 tail 節點, 並等到該元素被消費者消費了才返回.

  • tryTransfer: 這個方法用來試探生產者傳入的元素是否可以直接傳遞給消費者. 如果沒有消費者等待接收元素則返回 false. 和 transfer 方法的區別是 tryTransfer 方法無論是否有消費者接收, 方法立即返回. 而 transfer 方法是必須等到消費者消費了才返回.

7. LinkedBlockingDeque
  • 是一個鏈表結構的雙向阻塞隊列. 所謂雙向隊列指的是可以從隊列的兩端插入和移除元素. 雙向隊列因爲多了一個操作隊列的入口, 在多線程同時入隊時, 也就減少了一半的競爭.

  • 多了 addFirst, addLast, offerFirst ....等方法. 插入方法 add 等同於 addLast, 移除 remove 等同於 removeFirst. 但是 take 方法卻等同於 takeFirst.

  • 在初始化 LinkedBlockingDeque 時可以設置其容量防止過度膨脹.

Array 實現的隊列和 Linked 實現隊列的區別
  1. 隊列中鎖的實現不同
    • ArrayBlockingQueue實現的隊列中的鎖是沒有分離的, 即生產和消費用的是同一個鎖
    • LinkedBlockingQueue 實現的隊列中的鎖是分離的, 即生產用的是 putLock, 消費是 takeLock.
  2. 在生產或消費時操作不同
    • ArrayBlockingQueue 實現的隊列中在生產和消費的時候, 是直接將枚舉對象插入或移除的.
    • LinkedBlockingQueue 實現的隊列中在生產和消費的時候, 需要把枚舉對象轉換爲 Node<E> 進行插入或者移除. 會影響性能.
  3. 隊列大小初始化方式不同
    • ArrayBlockingQueue 實現的隊列中必須制定隊列的大小.
    • LinkedBlockingQueue 實現的隊列中可以不指定隊列的大小, 但是默認是 Integer.MAX_VALUE.

在 Android 開發中會使用到的估計也就只有 ArrayBlockingQueueLinkedBlockingQueue. 剩下的幾種感興趣的也可以瞭解一下.

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