10分鐘搞定 Java 併發佇列好嗎?好的
阿新 • • 發佈:2020-08-26
| **好看請贊,養成習慣**
> - 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想
>
> - If you can NOT explain it simply, you do NOT understand it well enough
現陸續將Demo程式碼和技術文章整理在一起 [Github實踐精選](https://github.com/FraserYu/learnings) ,方便大家閱讀檢視,本文同樣收錄在此,覺得不錯,還請Star
---
## 前言
如果按照用途與特性進行粗略的劃分,JUC 包中包含的工具大體可以分為 6 類:
1. 執行者與執行緒池
2. 併發佇列
3. 同步工具
4. 併發集合
5. 鎖
6. 原子變數
在[併發系列](https://dayarch.top/categories/Coding/Java-Concurrency/)中,主要講解了 `執行者與執行緒池`,`同步工具`,`鎖` , 在分析原始碼時,或多或少的提及到了「佇列」,佇列在 JUC 中也是多種多樣存在,所以本文就以「遠看」視角,幫助大家快速瞭解與區分這些看似「雜亂」的佇列
## 併發佇列
Java 併發佇列按照實現方式來進行劃分可以分為 2 種:
1. 阻塞佇列
2. 非阻塞佇列
如果你已經看完併發系列鎖的實現,你已經能夠知道他們實現的區別:
> 前者就是基於鎖實現的,後者則是基於 CAS 非阻塞演算法實現的
常見的佇列有下面這幾種:
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104538512-1987311664.png)
瞬間懵逼?看到這個沒有人性的圖想直接走人? 客觀先別急,一會就柳暗花明了
當下你也許有個問題:
**為什麼會有這麼多種佇列的存在**?
> 鎖有應對各種情形的鎖,佇列也自然有應對各種情形的隊列了, 是不是也有點單一職責原則的意思呢?
所以我們要了解這些佇列到底是怎麼設計的?以及用在了哪些地方?
先來看下圖
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104540932-505470422.png)
如果你在 IDE 中開啟以上非阻塞佇列和阻塞佇列,檢視其實現方法,你就會發現,`阻塞佇列`較`非阻塞佇列` **額外支援兩種操作**:
1. **阻塞的插入**
當佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿
2. **阻塞的移除**
當佇列為空時,獲取元素的執行緒會阻塞,直到佇列變為非空
綜合說明入隊/出隊操作,看似雜亂的方法,用一個表格就能概括了
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104541542-1898492282.png)
**丟擲異常**
- 當佇列滿時,此時如果再向佇列中插入元素,會丟擲 IllegalStateException (這很好理解)
- 當佇列空時,此時如果再從佇列中獲取元素,會丟擲 NoSuchElementException (這也很好理解)
**返回特殊值**
- 當向佇列插入元素時,會返回元素是否插入成功,成功則返回 true
- 當從佇列移除元素時,如果沒有則返回 null
**一直阻塞**
- 當佇列滿時,如果**生產者執行緒**向佇列 put 元素,佇列會一直阻塞生產者執行緒,直到佇列可用或者響應中斷退出
- 當佇列為空時,如果**消費者執行緒** 從佇列裡面 take 元素,佇列會阻塞消費者執行緒,直到佇列不為空
關於阻塞,我們其實早在 [併發程式設計之等待通知機制](https://dayarch.top/p/waiting-notification-mechanism.html) 就已經充分說明過了,你還記得下面這張圖嗎?原理其實是一樣一樣滴
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104542437-1983822963.png)
**超時退出**
和鎖一樣,因為有阻塞,為了靈活使用,就一定支援超時退出,阻塞時間達到超時時間,就會直接返回
至於為啥插入和移除這麼多種單詞表示形式,我也不知道,為了方便記憶,只需要記住阻塞的方法形式即可:
> 單詞 `put` 和 `take` 字母 `t` 首位相連,一個放,一個拿
到這裡你應該對 Java 併發佇列有了個初步的認識了,原來看似雜亂的方法貌似也有了規律。接下來就到了瘋狂串知識點的時刻了,藉助前序章節的知識,分分鐘就理解全部隊列了
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104542811-267882694.gif)
### ArrayBlockingQueue
之前也說過,JDK中的命名還是很講究滴,一看這名字,底層就是陣列實現了,是否有界,那就看在構造的時候是否需要指定 capacity 值了
填鴨式的說明也容易忘,這些都是哪看到的呢?在所有佇列的 Java docs 的第一段,一句話就概括了該佇列的主要特性,所以強烈建議大家自己在看原始碼時,簡單**瞄一眼** docs 開頭,心中就有多半個數了
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104544814-1897811766.png)
在講 [Java AQS佇列同步器以及ReentrantLock的應用](https://dayarch.top/p/java-aqs-and-reentrantlock.html) 時我們介紹了公平鎖與非公平鎖的概念,ArrayBlockingQueue 也有同樣的概念,看它的構造方法,就有 ReentrantLock 來輔助實現
```java
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
```
預設情況下,依舊是不保證執行緒公平訪問佇列(公平與否是指阻塞的執行緒能否按照阻塞的先後順序訪問佇列,先阻塞線訪問,後阻塞後訪問)
到這我也要臨時問一個說過多次的面試送分題了:
> 為什麼預設採用非公平鎖的方式?它較公平鎖方式有什麼好處,又可能帶來哪些問題?
知道了以上內容,結合上面表格中的方法,ArrayBlockingQueue 就可以輕鬆過關了
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104545136-1836728809.png)
和陣列相對的自然是連結串列了
### LinkedBlockingQueue
![](https://img2020.cnblogs.com/other/1583165/202008/1583165-20200826104545398-889900613.png)
LinkedBlockingQueue 也算是一個有界阻塞佇列 ,從下面的建構函式中你也可以看出,該佇列的預設和最大長度為 Integer.MAX_VALUE ,這也就 docs 說 optionally-bounded 的原因了
```java
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head =