問題是最好的老師!
一、思考
- 問題一:阻塞隊列阻塞了什麼操作?爲什麼需要這樣一個隊列(使用場景是什麼)?
我們都知道在線程池中會用到阻塞隊列,那麼線程池爲什麼不用一個一般的隊列,要用阻塞隊列。那麼阻塞隊列是否還有其他場景。
- 問題二:阻塞隊列是如何實現阻塞/喚醒的?以及何時阻塞/喚醒?
我們知道常見的阻塞喚醒方式有Object.wait/notify和Condition.await/signal。
- 問題二:多線程情況下,阻塞隊列是否是線程安全的,可以試想如果不加同步,那麼多個線程修改隊列的結構,勢必會出現問題,阻塞隊列是如何處理的?
即多線程情況下,入隊列和出隊列是否需要進行同步。
-
問題三:有哪幾種常見的阻塞隊列實現方式,其差異性在哪裏,各有什麼優缺點,如何選擇?
比如我們在使用線程池時,選用什麼阻塞隊列比較合適。其默認的阻塞隊列是什麼方式的。
- 問題四:線程池中阻塞隊列設置多大合適?依據是什麼?
以上問題等分析完源碼之後,統一分析,也會在源碼分析過程中穿插分析。
二、源碼解讀
-
註釋翻譯
在不瞭解阻塞隊列設計初衷的情況下,可以參考阻塞隊列(BlockingQueue)註釋,瞭解其基本作用,其翻譯如下:
阻塞隊列是JAVA隊列中的一種(先進先出),但是其提供額外操作:
1.支持當從隊列中獲取元素時,如果隊列爲空,則進行等待直到隊列不爲空返回元素。
2.支持當向隊列中添加元素時,如果隊列已滿,則進行等待直到隊列有可用空間入隊。
對於阻塞隊列的不同操作(方法),當其不能立即成功時(比如獲取一個元素時,隊列可能爲空),會表現出四種形式,但是這些操作在未來的某一個時間點會成功(比如當隊列不爲空,獲取數據成功),這四種表現形式如下:
- Throws an Exception(拋出一個異常)。
- Special Value(返回一個值,獲取元素時返回元素,成功添加到隊列返回true等待,可能返回null和fasle,取決於具體操作)。
- Blocks(阻塞當前線程,直到操作可以成功)。
- Time Out(在放棄當前操作前,阻塞給定時間)。
入隊操作包含以下方法,後續會有源碼分析:
- boolean add(e):添加元素到隊尾,添加成功返回true,若當前無可用空間拋出異常。
- boolean offer(e):添加元素到隊尾,添加成功返回true,若當前無可用空間返回fasle。
- void put(e):添加元素到隊尾,如果無可用空間則等待直到空間可用。
- boolean offer(e,time,unit):添加元素到隊尾,若無可用空間,等待指定時間,若等待時間到,仍無可用空間,返回false,否則返回true。
出隊操作(移除元素)包含以下方法,後續會有源碼分析:
- Boolean remove(e):從隊列中移除指定元素,如果包含指定元素且移除成功,返回true,否則返回fasle。
- E poll():返回對頭元素,如果隊列爲空返回null。
- E take():返回對頭元素,如果隊列爲空則等待,直到隊列中有元素。
- E poll(time,unit):返回對頭元素,如果隊列爲空則等待指定時間,若等待時間到,隊列中仍無元素,返回null。
返回對頭元素(不移除元素):
- peek():隊列爲空返回null,不爲空返回對頭元素。
- element():隊列爲空拋出異常,不爲空返回對頭原色。
總的來說,對應上面翻譯的四種表現形式,對於入隊操作,要麼成功,要麼失敗,要麼阻塞,要麼拋出異常,對於出隊操作要麼返回對應元素,要麼返回null,要麼阻塞。
阻塞隊列不接受空元素(null)入隊,因爲null有特殊意義,其表示poll操作如果隊列爲空的返回標識。
阻塞隊列的實現是線程安全的。
-
常見實現
阻塞隊列的實現(JUC包下,本文不包括Deque雙向隊列的實現部分分析),包括以下幾種,分別對應不同功能:
- ArrayBlockingQueue:底層數據結構爲數組的先進先出阻塞隊列,有限。
- LinkedBlockingQueue:底層數據結構爲鏈表的先進先出阻塞隊列,有限/無限(int最大值)可自主選擇。
- PriorityBlockingQueue:優先級隊列,底層數據結構爲數組的阻塞隊列,無限。
- LinkedTransferQueue:底層數據結構爲鏈表的先進先出阻塞隊列,生產者會等待消費者消費,無限。
- DelayQueue:延遲隊列,每個元素過期之後,才能被取走。
- SynchronousQueue:入隊操作必須等待與之相關的出隊操作完成,反之亦然。
-
源碼解讀
先從最基本的阻塞隊列開始分析
-
ArrayBlockingQuque
- 屬性
//隊列中存在的元素個數
int count;
//隊列存儲元素的數據結構
final Object[] items;
//以下兩個屬性是出於這樣的考慮:
//對於數組來說,如果對頭元素下標始終是0,則每次移除對頭元素需要移動後續所有元素,性能不高
//使用下面兩個屬性即該數組相當於循環數組,不涉及數據的移動
//下一次出隊元素的位置:對頭元素所在位置,每次出隊之後該值加一,超過容量爲0
int takeIndex;
//下一次入隊元素的位置:隊尾元素的後一個位置,超過容量爲0
int putIndex;
//實現線程安全操作
final ReentrantLock lock;
//阻塞隊列的核心在於支持
//1.入隊時,無可用空間則阻塞:對應notFull
//2.出隊時,無可用元素則阻塞:對應notEmpty
//獲取元素時阻塞/喚醒條件
private final Condition notEmpty;
//插入元素時阻塞/喚醒條件
private final Condition notFull;
- 構造函數
//數組形式的阻塞隊列爲有限隊列,其容量爲capacity。
public ArrayBlockingQueue(int capacity) {
//請看下文的構造函數
this(capacity, false);
}
//capacity:隊列容量
//fair:是否公平鎖
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//直接初始化數組大小爲容量上限
this.items = new Object[capacity];
//默認採用非公平的可重入鎖,實現線程安全
lock = new ReentrantLock(fair);
//初始化Condition
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
//該構造函數支持根據傳入的集合,初始化隊列
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
//循環入隊
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
//此時takeIndex肯定爲0
//如果隊列已滿,那下一次入隊肯定是從對頭開始,因爲出隊必定在對頭,只有對頭纔有空閒位置,隊列未滿,則putIndex未i,即隊尾元素的下一個位置。
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
- 主要方法
入隊主要操作:
boolean add(e):入隊,隊列已滿則拋出異常
public boolean add(E e) {
//調用父類AbstractQueue的add(e)方法,請看下文
return super.add(e);
}
//可以看出該方法主要調用了offer(e)方法,詳情請看offer(e)
方法詳解
//offer方法隊列爲滿返回fasle,該方法則再次封裝拋出異常而已
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
boolean offer(e):入隊,隊列爲滿返回false
public boolean offer(E e) {
//元素不能爲空,爲什麼不能爲空,前面翻譯中有解釋
checkNotNull(e);
final ReentrantLock lock = this.lock;
//線程安全
lock.lock();
try {
//隊列已滿,直接返回false,put方法則不同,隊列已滿就會阻塞,具體請看下文對其實現
if (count == items.length)
return false;
else {
//否則入隊,詳情請看入隊操作
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
void enque(e):入隊操作
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入數據到putIndex位置
items[putIndex] = x;
//如果隊列已滿,更新下一個插入位置爲0,因爲這個數組是一個循環數組
if (++putIndex == items.length)
putIndex = 0;
count++;
//阻塞隊列的出隊操作如果隊列沒有元素會在notEmpty上進行阻塞,此處只有元素入隊就進行喚醒
//這樣只有有線程在等待該條件就能被喚醒其中一個
notEmpty.signal();
}
boolean offer(e,time,unit):入隊,如果隊列已滿,則等待time時間,之後如果隊列還是已滿,則返回false。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//這裏可響應中斷
lock.lockInterruptibly();
try {
//循環的目的
//1.使等待時間不小於nanos秒
//2.等待時間到之後,再次判斷隊列是否已滿
while (count == items.length) {
if (nanos <= 0)
//等待時間到,隊列還是滿,返回fasle
return false;
//返回的nanos爲已等待nanos-nanos秒,剩餘nanos秒
//注意這個地方也是會響應notFull.signal()喚醒操作的
nanos = notFull.awaitNanos(nanos);
}
//隊列未滿入庫,請看上文enque(e)方法
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
void put(e):入隊,隊列已滿則一直等待
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//循環的目的是通知後進行二次確認,因爲在await()時,鎖已經被釋放,可能其他線程被通知,已經put
while (count == items.length)
//隊列已滿,則等待notFull.signal()
notFull.await();
//隊列未滿,重新入隊,這個時候可以安全入隊,因爲await()被喚醒之後,該線程又重新獲得鎖
enqueue(e);
} finally {
lock.unlock();
}
}
出隊主要操作:
E poll():出隊操作,隊列不爲空,返回takeIndex位置元素,否則返回null
public E poll() {
final ReentrantLock lock = this.lock;
//線程安全
lock.lock();
try {
//隊列爲空返回null,否則出隊,請看下文dequeue方法
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
E dequeue():出隊頭元素:
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//返回takeIndex位置元素
E x = (E) items[takeIndex];
items[takeIndex] = null;
//循環數組
if (++takeIndex == items.length)
takeIndex = 0;
count--;
//暫時不分析
if (itrs != null)
itrs.elementDequeued();
//出隊之後,喚醒可能在notFull上阻塞的線程,表示隊列未滿,可以入隊
notFull.signal();
return x;
}
E poll(time,unit):出隊,隊列爲空等待time時間,時間到,隊列爲空,則返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//可響應中斷
lock.lockInterruptibly();
try {
//隊列爲空,等待
while (count == 0) {
//隊列爲空,無剩餘等待時間則返回null,如果依然不能理解,請看上文offer的解釋,依然不能理解,請看另一篇博文線程池中的解釋
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//隊列不爲空,出隊,請看上午dequeue方法
return dequeue();
} finally {
lock.unlock();
}
}
E take():出隊,隊列爲空則等待直到隊列中有元素返回:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//這裏循環的目的有點類似於put,因爲await會釋放鎖,所以需要二次判斷
while (count == 0)
//這裏區別於offer(time,unit)的awaitNanos(time,unit)
notEmpty.await();
//隊列不爲空,出隊
return dequeue();
} finally {
lock.unlock();
}
}
三、問題分析(回答開篇)
上文源碼解釋中分析了ArrayBlockingQueue阻塞隊列的各個方法的具體實現,對於LinkedBlockingQueue,數據結構不同,思想相同,對於其他類型的隊列,在此思想的基礎上,進行擴展,添加了一些格外功能。
- 問題一:阻塞隊列阻塞了什麼操作?爲什麼需要這樣一個隊列(使用場景是什麼)?
1.阻塞隊列支持入隊,隊列如果已滿進行阻塞;出隊隊列爲空,進行阻塞的操作。
2.我們控制這樣一個隊列,可以支持高併發下數據處理(其線程安全),以及數據緩存(可爲有限隊列)。
3.線程池中用阻塞隊列不用一般隊列,是爲了更好的實現線程的複用,當隊列中無可用元素時,核心線程可一直阻塞,當有元素可用時進行處理。試想如果不用這樣的阻塞形式,一直循環判斷隊列是否爲空判斷隊列是否有元素,那就相當於程序中寫了一個死循環。
- 問題二:阻塞隊列是如何實現阻塞/喚醒的?以及何時阻塞/喚醒?
通過上面的源碼分析,我們瞭解到,阻塞隊列的阻塞和喚醒是通過Conditon.await/signal實現。
因爲分別需要對應入隊和出隊的阻塞喚醒操作,所以維護了兩個Conditon條件,分別如下:
1.notEmpty:控制出隊,出隊時,隊列爲空,則notEmpty.await()等待,當有元素入隊時notEmpty.signal()喚醒等待出隊線程。
2.notFull:控制入隊,入隊時,對列已滿,則notFull.await()等待,當有元素出隊時notFull.signal()喚醒等待入隊線程。
- 問題二:多線程情況下,阻塞隊列是否是線程安全的,可以試想如果不加同步,那麼多個線程修改隊列的結構,勢必會出現問題,阻塞隊列是如何處理的?
通過源碼分析,已經瞭解到阻塞隊列的入隊和出隊都是線程安全的,通過ReentrantLock加鎖。
-
問題三:有哪幾種常見的阻塞隊列實現方式,其差異性在哪裏,各有什麼優缺點,如何選擇?
前文中已經提到常見阻塞隊列的實現,並簡單介紹了其作用。
1.ArrayBlockingQueue:因爲其是有界的,所以當需要有界隊列時,可以選擇使用他,因爲其實現是循環數組,所以插入不需要移動元素,效率也比較高。
2.LinkedBlockingQueue:當未設置容量時,是無解隊列,所以想用無界隊列,可以選用它,但是因爲隊列需要維護額外的指針,可能佔用的空間相對多一些。
2.其他隊列:其他隊列都有一些各自的特性功能,如果需要用到這些功能,則選用這些隊列。
- 問題四:線程池中阻塞隊列設置多大合適?依據是什麼?
暫未理解,,後續分析,如果有知道的,歡迎評論..謝謝。