Java中的阻塞佇列
1. 什麼是阻塞佇列
阻塞佇列相比普通佇列,支援下面兩個操作:
- 支援阻塞的插入方法。佇列滿時,插入元素的執行緒可以阻塞等待佇列變為不滿。
- 支援阻塞的移除方法。佇列為空時,獲取元素的執行緒可以阻塞等待佇列變為非空。
阻塞佇列常常用於生產者消費者場景。
在阻塞佇列不可用時,這兩個附加操作提供了四種處理方式:
方法/處理方式 | 丟擲異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
- 丟擲異常:當佇列滿時再插入元素,會丟擲
IllegalStateException("Queuefull")
異常;當佇列空時再獲取元素,會丟擲NoSuchElementException
異常。 - 返回特殊值:當往佇列插入元素時,插入成功返回 true。當從佇列取出元素時,沒有則返回 null。
- 一直阻塞:當阻塞佇列滿時,如果
put
元素,佇列會一直阻塞生產者執行緒,直到佇列可用或者響應中斷退出。當佇列空時,如果消費者執行緒從佇列take
元素,佇列會阻塞消費者執行緒,知道佇列不為空。 - 超時退出:當阻塞佇列滿時,如果生產者執行緒往佇列裡插入元素,佇列會阻塞生產者執行緒一段時間,如果超過指定時間,生產者執行緒就會退出。
2. Java中的阻塞佇列
JDK7 提供了下列 7 個阻塞佇列:
-
ArrayBlockingQueue。一個由陣列結構組成的有界阻塞佇列。
-
LinkedBlockingQueue。一個由連結串列結構組成的有界阻塞佇列。
-
PriorityBlockingQueue。一個支援優先順序排序的無界阻塞佇列。
-
DelayQueue。一個使用優先順序佇列實現的無界阻塞佇列。
-
SynchronousQueue。一個不儲存元素的阻塞佇列。
-
LinkedTransferQueue。一個由連結串列結構組成的無界阻塞佇列。
-
LinkedBlockingDeque。一個由連結串列結構組成的雙向阻塞佇列。
2.1 ArrayBlockingQueue
ArrayBlockingQueue
是一個陣列實現的有界阻塞佇列,按照 FIFO 原則對元素排序。
預設狀態下執行緒競爭該佇列是不公平的,但可以用以下程式碼建立一個公平的阻塞佇列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
公平性使用一個公平的可重入鎖實現的。
2.2 LinkedBlockingQueue
LinkedBlockingQueue
是一個連結串列實現的有界阻塞佇列。
預設和最大長度是Integer.MAX_VALUE
,按照先入先出原則進行排序。
2.3 PriorityBlockingQueue
PriorityBlockingQueue
是支援優先順序的無界阻塞佇列,預設情況下元素自然順序升序排列,也可以自定義Comparator
。
2.4 DelayQueue
DelayQueue
是一個支援延時獲取元素的無界阻塞佇列。佇列使用PriorityQueue
實現,佇列中的元素必須實現Delayed
介面,在建立元素時指定多久才能從佇列中獲取當前元素。
可以用在以下場景:
- 快取系統的設計。可以用
DelayQueue
儲存快取元素的有效期,使用一個執行緒迴圈查詢DelayQueue
,一旦能獲取元素,說明有效期到了。 - 定時任務排程。可以用
DelayQueue
儲存當前將執行的任務和執行時間,一旦從DelayQueue
獲取任務就開始執行,比如TimerQueue
就是用DelayQueue
實現的。
2.5 SynchronousQueue
SynchronousQueue
是一個不儲存元素的阻塞佇列,每一個put
操作必須等待一個take
操作,否則不能繼續put
。
SynchronousQueue
可以看成一個傳球手,負責把生產者執行緒處理的資料直接傳遞給消費者執行緒。佇列本身不儲存元素,非常適合傳遞性場景,吞吐量很高。
2.6 LinkedTransferQueue
LinkedTransferQueue
是一個連結串列結構的無界阻塞佇列。相對於其他阻塞佇列,LinkedTransferQueue
多了tryTransfer()
和transfer()
方法。
transfer()
方法
如果當前有消費者正在等待接收元素,比如take()
方法或poll()
方法,transfer()
方法可以把生產者傳入的元素立即transfer()
給消費者。如果沒有消費者在等待接收元素,transfer()
方法會將元素存放在佇列的tail
節點,並等到該元素被消費者消費了才返回。
tryTransfer()
方法
tryTransfer()
方用來試探生產者傳入的元素能否直接傳給消費者。如果沒有消費者等待接收元素,就返回false
。
tryTransfer()
方法無論消費者是否接收,方法立即返回,而transfer()
方法必須等待消費者消費了才返回。
2.7 LinkedBlockingDeque
LinkedBlockingDeque
是一個由連結串列結構組成的雙向阻塞佇列。
由於是雙向佇列,多了addFirst()
、addLast()
、offerFirst()
等方法。
這個佇列可以用於工作竊取模式中。
3. 阻塞佇列實現原理
JDK 使用通知模式實現阻塞佇列。
-
當生產者往滿的佇列裡新增元素時,會阻塞住生產者。
-
當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。
上面的兩步在 JDK 中使用了 Condition 實現。
當往佇列裡插入元素,而佇列不可用時,阻塞生產者主要通過LockSupport.park(this)
來實現。