## 25 整體設計:隊列設計思想、工作中使用場景
## 引導語
本章我們學習了 LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue、DelayQueue 四種隊列,四種隊列底層數據結構各不相同,使用場景也不相同,本章我們從設計思想和使用場景兩個大的方向做一些對比和總結。
### 1 設計思想
首先我們畫出隊列的總體設計圖:

從圖中我們可以看出幾點:
1. 隊列解耦了生產者和消費者,提供了生產者和消費者間關系的多種形式,比如 LinkedBlockingQueue、ArrayBlockingQueue 兩種隊列就把解耦了生產者和消費者,比如 SynchronousQueue 這種就把生產者和消費者相互對應(生產者的消息被消費者開始消費之后,生產者才能返回,為了方便理解,使用相互對應這個詞);
2. 不同的隊列有著不同的數據結構,有鏈表(LinkedBlockingQueue)、數組(ArrayBlockingQueue)、堆棧(SynchronousQueue)等;
3. 不同的數據結構,決定了入隊和出隊的姿勢是不同的。
接下來我們分別按照這幾個方面來總結分析一下。
#### 1.1 隊列的數據結構
鏈表結構的隊列就是 LinkedBlockingQueue,其特征如下:
1. 初始大小默認是 Integer 的最大值,也可以設置初始大小;
2. 鏈表元素通過 next 屬性關聯下一個元素;
3. 新增是從鏈表的尾部新增,拿是從鏈表頭開始拿。
數組結構的隊列是 ArrayBlockingQueue,特征如下:
1. 容量大小是固定的,不能動態擴容;
2. 有 takeIndex 和 putIndex 兩個索引記錄下次拿和新增的位置;
3. 當 takeIndex 和 putIndex 到達數組的最后一個位置時,下次都是從 0 開始循環。
SynchronousQueue 有著兩種數據結構,分別是隊列和堆棧,特征如下:
1. 隊列保證了先入先出的數據結構,體現了公平性;
2. 堆棧是先入后出的數據結構,是不公平的,但性能高于先入先出。
#### 1.2 入隊和出隊的方式
不同的隊列有著不同的數據結構,導致其入隊和出隊的方式也不同:
1. 鏈表是入隊是直接追加到隊尾,出隊是從鏈表頭拿數據;
2. 數組是有 takeIndex 和 putIndex 兩個索引位置記錄下次拿和取的位置,如總體設計圖,入隊直接指向了 putIndex,出隊指向了 takeIndex;
3. 堆棧主要都是圍繞棧頭進行入棧和出棧的。
#### 1.3 生產者和消費者之間的通信機制
從四種隊列我們可以看出來生產者和消費者之間有兩種通信機制,一種是強關聯,一種是無關聯。
強關聯主要是指 SynchronousQueue 隊列,生產者往隊列中 put 數據,如果這時候沒有消費者消費的話,生產者就會一直阻塞住,是無法返回的;消費者來隊列里取數據,如果這時候隊列中沒有數據,消費者也會一直阻塞住,所以 SynchronousQueue 隊列模型中,生產者和消費者是強關聯的,如果只有其中一方存在,只會阻塞,是無法傳遞數據的。
無關聯主要是說有數據存儲功能的隊列,比如說 LinkedBlockingQueue 和 ArrayBlockingQueue,只要隊列容器不滿,生產者就能放成功,生產者就可以直接返回,和有無消費者一點關系都沒有,生產者和消費者完全解耦,通過隊列容器的儲存功能進行解耦。
### 2 工作中的使用場景
在日常工作中,我們需要根據隊列的特征來匹配業務場景,從而決定使用哪種隊列,我們總結下各個隊列適合使用的場景:
#### 2.1 LinkedBlockingQueue
適合對生產的數據大小不定(時高時低),數據量較大的場景,比如說我們在淘寶上買東西,點擊下單按鈕時,對應著后臺的系統叫做下單系統,下單系統會把下單請求都放到一個線程池里面,這時候我們初始化線程池時,一般會選擇 LinkedBlockingQueue,并且設置一個合適的大小,此時選擇 LinkedBlockingQueue 主要原因在于:在不高于我們設定的閾值內,隊列里面的大小可大可小,不會有任何性能損耗,正好符合下單流量的特點,時大時小。
一般工作中,我們大多數都會選擇 LinkedBlockingQueue 隊列,但會設置 LinkedBlockingQueue 的最大容量,如果初始化時直接使用默認的 Integer 的最大值,當流量很大,而消費者處理能力很差時,大量請求都會在隊列中堆積,會大量消耗機器的內存,就會降低機器整體性能甚至引起
宕機,一旦宕機,在隊列中的數據都會消失,因為隊列的數據是保存在內存中的,一旦機器宕機,內存中的數據都會消失的,所以使用 LinkedBlockingQueue 隊列時,建議還是要根據日常的流量設置合適的隊列的大小。
#### 2.2 ArrayBlockingQueue
一般用于生產數據固定的場景,比如說系統每天會進行對賬,對賬完成之后,會固定的產生 100 條對賬結果,因為對賬結果固定,我們就可以使用 ArrayBlockingQueue 隊列,大小可以設置成 100。
#### 2.3 DelayQueue
延遲隊列,在工作中經常遇到,主要用于任務不想立馬執行,想等待一段時間才執行的場景。
比如說延遲對賬,我們在工作中曾經遇到過這樣的場景:我們在淘寶上買東西,彈出支付寶付款頁面,在我們輸入指紋的瞬間,流程主要是前端 -》交易后端 -》支付后端,交易后端調用支付后端主要是為了把我們支付寶的錢劃給商家,而交易調用支付的過程中,有小概率的情況,因為網絡抖動會發生超時的情況,這時候就需要通過及時的對賬來解決這個事情(對賬只是解決這個
問題的手段之一),我們簡單畫一個流程圖:

這是一個真實場景,為了方便描述,已經大大簡化了,再說明幾點:
1. 交易調用支付的接口,這個接口的作用就是為了把小美的 800 元轉給商家小明;
2. 接口調用超時,此時交易系統并不知道 800 有沒有成功轉給小明,當然想知道的方式有很多,我們選擇了對賬的方式,對賬的目的就是為了知道當前 800 元有沒有成功轉給小明;
3. 延遲對賬的目的,因為支付系統把 800 元轉給商家小明也是需要時間的,如果超時之后立馬對賬,可能轉賬的動作還在進行中,導致對賬的結果不準確,所以需要延遲幾秒后再去對賬;
4. 對賬之后的結果有幾種,比如已經成功的把 800 元轉給小明了,這時候需要把對賬結果告訴交易系統,交易系統更新數據,前端就能夠顯示轉賬成功了。
在這個案列中,延遲對賬的核心技術就是 DelayQueue,我們大概這么做的:新建對賬任務,設置 3 秒之后執行,把任務放到 DelayQueue 中,過了 3 秒之后,就會自動執行對賬任務了。
DelayQueue 延遲執行的功能就在這個場景中得到應用。
#### 3 總結
我們不會為了閱讀源碼而讀源碼,我們讀源碼的最初目的,是為了提高我們的技術深度,最終目的是為了在不同的場景中,能夠選擇合適的技術進行落地,本章中解釋的一些隊列的場景,我們在工作中其實都會遇到,特別是在使用線程池時,使用哪種隊列是我們必須思考的一個問題,所以本章先比較了各個隊列的適合使用場景,然后舉了幾個案列進行具體分析,希望大家也能把技術具體落地到實際工作中,使技術推動、輔助業務。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題