前言
在前文中非阻塞隊列之ConcurrentLinkedQueue源碼解析中,深度解析了非阻塞隊列的源碼。本篇內容將對於阻塞隊列的原理、4中處理方式以及7中阻塞隊列進行詳細解析。
什麼是阻塞隊列
首先,再一次申明,隊列必須是線程安全的,否則將毫無意義。阻塞隊列最大的特徵就是提供兩種阻塞操作:
- 阻塞的插入元素:當隊列滿時,隊列會阻塞插入元素的線程,直到隊列非滿;
- 阻塞的獲取元素:對隊列空時,隊列會阻塞獲取元素的線程,直到隊列非空。
說到這裏,其實要研究Java中阻塞隊列的核心問題就付出水面了:
- 阻塞隊列如何實現阻塞操作的?
- 如何在達到一定條件時喚醒相關線程的?
- 如何保證線程安全的插入元素和獲取元素?
其實這就回到了併發要解決的本質。在Java併發——線程安全一文中對線程安全和如何實現線程安全有非常清晰的闡述。
要實現以上幾種功能的方案有很多:採用Object.wait/notify或者基於AQS。Java中大多數的阻塞隊列採用的基於AQS實現的ReentrantLock和Condition的方式實現線程安全的。阻塞隊列的源碼比起非阻塞隊列的源碼要簡單很多,如果對於這些基本理念很熟悉的話,那麼理解Java阻塞隊列的源碼就很簡單了。
4種處理方式
方法/處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | pull() | take() | take(time, unit) |
檢查方法 | element() | peek() | - | - |
- 拋出異常:當隊列滿時,再添加元素的話將拋出IllegalStateException(“Queue full”)異常;當隊列空時,在移除元素的話將拋出NoSuchElementException異常。
- 返回特殊值:添加元素方法會返回boolean值表示添加成功與否,如果返回true表示添加成功,如果隊列滿了,同理,如果移除元素成功也將返回false。
- 一直阻塞:當隊列滿時,隊列會阻塞所有添加元素的線程,直到線程非滿;當隊列空時,隊列會阻塞所有移除元素的線程,直到線程非空。
- 超時退出:當阻塞超過一段時間之後,線程會自動退出。
注意,阻塞隊列分爲有界阻塞隊列和無界阻塞隊列,對於無界阻塞隊列而言,永遠不會出現隊列滿的情況,因此put/offer/take/pull這些方法不會出現阻塞的情況。當然無界並不意味着可以存放無限的元素,畢竟JVM內存是有界的!
在實際開發中,這四種處理方式改如何選擇呢?
拋出異常:這種方式適用於“一次性”場景,比如中獎活動,規定只能有10名用戶中獎,那麼隊列滿之後,將直接拋出異常拒絕再添加中獎用戶中隊列中,然後觸發派獎線程,派獎線程從隊列中獲取元素直到全部獲取完畢拋出異常結束派獎。
返回特殊值:這種場景適用於高併發、耗時短的任務。由於任務執行耗時短,當添加或者移除失敗時,可以採用自旋思想,自旋添加或者移除直到成功,這樣做的好處是避免了線程調度的性能消耗。
一直阻塞:這種場景適用於高併發、耗時長的任務。由於耗時長,此時再採用自旋的方式顯然不如阻塞線程。
超時退出:這種場景適用於高併發且允許操作失敗的場景,比如用戶行爲收集等,雖然無法保證100%的收集,但是在大量數據下90%以上的收集率足夠得到準確的數據分析結果了。相當於犧牲了一定的準確率以提升性能。
7種阻塞隊列
ArrayBlockingQueue
基於數組實現的有界隊列,FIFO。內部使用的是ReentrantLock + ConditionObject實現的同步機制。支持線程公平的訪問隊列(本質上是設置ReentrantLock的公平鎖)
LinkedBlockingQueue
基於鏈表實現的有界隊列,FIFO。內部使用的是ReentrantLock + ConditionObject實現的同步機制。但是它不支持設置公平鎖。
PriorityBlockingQueue
是一個支持優先級的無界阻塞隊列。默認情況下是按照元素添加的順序升序排序的。也可以自定義類實現compareTo()方法來確定元素的排序規則。內部使用的是ReentrantLock + ConditionObject實現的同步機制。既然都已經支持優先級了,那麼自然不需要公平競爭咯。
DelayQueue
延時隊列。內部實際上是基於PriorityQueue實現的。隊列中的元素必須實現Delayed接口,在創建元素時可以指定延時多久才能從隊列中獲取到當前元素。
DelayQueue非常有用!我們可以基於DelayQueue實現以下場景:
- 緩存系統的設計:循環從延時隊列中獲取元素,如果能夠獲取到元素,說明這個元素的有效期到了;
- 定時任務系統的設計:循環從延時隊列中獲取元素,一定獲取到元素就執行相關的定時任務邏輯。在Java中,TimeQueue就是基於DelayQueue實現的。
Delayed接口的具體使用可以參考Java定時任務框架ScheduledThreadPoolExecutor中的ScheduledFutureTask。以後有機會可以進行定時任務系統專題研究。
SynchronousQueue
這是一個不存儲元素的隊列,需要注意的是每一個put操作都必須有對應的take操作,否則將會被阻塞不能夠繼續添加元素。這個隊列可以看做是容量只有1的隊列,非常適合一些傳遞性場景。它也是基於ReentrantLock和ConditionObject實現的。
LinkedTransferQueue
基於鏈表的無界阻塞隊列,FIFO。相比於其他阻塞隊列,它的特性就在於“transfer”。
- transfer方法
如果有消費端正在等待接收元素(take()/poll()方法),transfer方法可以將元素立即傳遞給消費端。如果此時沒有消費端,則transfer方法會將此元素放在隊列的tail節點,並且阻塞直到此元素被消費。 - tryTransfer方法
與transfer方法不同,此方法的目的是爲了試探元素能否直接被消費端接收。如果沒有消費端正在等待接收元素,此方法返回false。和transfer()方法不同,此方法會立即返回。
LinkedBlockingDeque
基於雙向鏈表的阻塞隊列。相比於其他阻塞隊列,他的特性就在於“雙向”。即:可以從隊列的兩端插入和移除元素。這樣就相當於減少了一半的鎖競爭,進一步提升了併發能力。LinkedBlockingDeque非常適用於高併發場景以及“工作竊取”模式中。
總結
本篇文章主要記錄我在學習阻塞隊列時的歷程和一些心得。通過本篇文章,我們知道了阻塞隊列的內部數據結構以及不同阻塞隊列的特點。在實際工作中我們可以根據實際情況來選擇合適的隊列讓程序更加合理。