(接上文《源碼閱讀(33):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(3)》)
2.3.3.3、forEachRemaining() 方法
forEachRemaining(Consumer<? super E> action) 方法是JDK 1.8+之後的版本新增的一個方法,是java.util.Iterator接口中定義的一個新方法,該方法類似於java.lang.Iterable接口定義的forEach(Consumer<? super T> action)方法。但是兩者是有區別的:
- forEach(Consumer<? super T> action)方法在java.lang.Iterable接口中被定義,表示一個可迭代的java Collection Framework具體集合類(實際上java.lang.Iterable接口比java.util.Collection接口層次更低)。調用者對具體集合類的forEach方法調用多少次,後者就會執行多少次:
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.add("1");
// ......
queue.add("6");
queue.add("7");
// 第一次執行forEach,並且會執行
queue.forEach(item -> {
// ......
});
// 第二次執行forEach,並且會執行
queue.forEach(item -> {
// ......
});
- forEachRemaining(Consumer<? super E> action) 方法在java.util.Iterator接口中被定義,後者表示一個具體迭代器的實現。無論調用者對具體迭代的的forEachRemaining(Consumer<? super E> action) 方法調用多少次,後者只會執行一次:
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.add("1");
// ......
queue.add("7");
// 創建一個迭代器
Iterator<String> itr = queue.iterator();
// 第一次執行forEachRemaining,並且會執行
itr.forEachRemaining(item -> {
// ......
});
// 第二次執行forEachRemaining,但是會執行
itr.forEachRemaining(item -> {
// ......
});
究其原因,是因爲forEachRemaining(Consumer<? super E> action) 方法默認基於迭代器的hasNext()方法和next()方法配合進行工作,在迭代器對象完成所有的數據的遍歷後,第二次調用同一個迭代器對象的forEachRemaining(Consumer<? super E> action) 方法,當然就不會執行了。以下是forEachRemaining方法的默認實現:
public interface Iterator<E> {
// ......
default void forEachRemaining(Consumer<? super E> action) {
// Consumer過程必須定義,否則就拋出異常
Objects.requireNonNull(action);
// 這裏就是hasNext()方法和next()方法的配合工作
while (hasNext()) {
action.accept(next());
}
}
// ......
}
由於ArrayBlockingQueue內部的循環數組結構和多線程場景下的工作要求,所以ArrayBlockingQueue隊列的迭代器中,對forEachRemaining方法的定義進行了調整,如下所示:
private class Itr implements Iterator<E> {
// ......
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
try {
final E e = nextItem;
if (e == null) return;
if (!isDetached()) {
incorporateDequeues();
}
// 以上代碼片段是操作方法獲取到了ArrayBlockingQueue隊列的操作權後的規範化處理過程
// 這裏就不再進行贅述。我們主要從action.accept(e);這句代碼開始介紹
// 該語句將上次next()方法記錄的nextItem數據輸出給下一消費者(Consumer)
action.accept(e);
// 這裏還要再進行一次是否過期的判定,因爲incorporateDequeues()方法運行後,當前Itr迭代器可能已經無法取數
// 具體的場景可參見上一篇文章的描述
if (isDetached() || cursor < 0) {
return;
}
final Object[] items = ArrayBlockingQueue.this.items;
// 接着基於當前有效的cursor遊標位置,開始對ArrayBlockingQueue隊列中還沒有遍歷(且可遍歷)的數據進行遍歷
for (int i = cursor, end = putIndex, to = (i < end) ? end : items.length; ; i = 0, to = end) {
for (; i < to; i++) {
action.accept(itemAt(items, i));
}
if (to == end) break;
}
} finally {
// 當完成所有數據的遍歷後(無論成功還是失敗)
// 將當前迭代器設置爲“獨立/無效”工作模式
cursor = nextIndex = lastRet = NONE;
nextItem = lastItem = null;
detach();
lock.unlock();
}
}
// ......
}
2.3.4、Itrs迭代器組的清理過程
本小節我們來詳細講解一下Itrs迭代器組的清理過程。上文已經提到,ArrayBlockingQueue隊列集合中所有的迭代器都在Itrs迭代器組中進行管理,這些迭代器將在Itrs迭代器組中以單向鏈表的方式進行排列。所以ArrayBlockingQueue隊列需要在特定的場景下,對已經失效、甚至已經被垃圾回收的迭代器管理節點進行清理。
例如,當ArrayBlockingQueue隊列有新的迭代器被創建時(併爲非獨立/無效工作模式),Itrs迭代器組就會嘗試清理那些無效的迭代器,其工作邏輯主要由Itrs.doSomeSweeping(boolean)方法進行實現,代碼片段如下所示:
/**
* 該方法負責對迭代器管理組Itrs進行清理。如果清理過程中發現了某個迭代器管理節點Itrs.Node
* 需要被清理,則掃除過程會更努力的打掃後續的節點——“小掃除”變“大掃除”,當前掃除次數也會被重置
* @param tryHarder 該方法採用“大掃除”還是“小掃除”方式進行清理。爲true的時候,表示使用“大掃除”模式
*/
void doSomeSweeping(boolean tryHarder) {
// assert lock.isHeldByCurrentThread();
// assert head != null;
// 小掃除只會對單向鏈表清理4次,大掃除會至少清理16次
int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
Node o, p;
// 從這個變量的單詞就可以知道,這是一臺掃除車
// 這臺掃除車,將順着迭代器組中的單向隊列“向前開”
final Node sweeper = this.sweeper;
// 這是一個標記,最直白的理解就是“掃除車”是否開車
boolean passedGo;
// sweeper(垃圾車)爲null的情況,主要的場景是Itrs迭代器組中沒有迭代器對象
// 也可能是上次的清理操作已經將單向鏈表中所有的Node節點掃除完畢
if (sweeper == null) {
// 從當前單向鏈表的第一個Node節點開始,順着鏈表向後打掃
o = null;
p = head;
passedGo = true;
} else {
// 從上次掃除結束的Node節點開始,繼續順着單向鏈表向後打掃
o = sweeper;
p = o.next;
passedGo = false;
}
// ============ 以上過程決定“掃除車”從哪個地方開始打掃。以下就開始進行具體的打掃了。
// 首先根據之前已經確認的掃除次數,決定是“大掃除”還是“小掃除”,16次清掃(循環)還是4次清掃(循環)
// 注意,如果在清掃過程中,發現已經被回收或者已經“無效”的迭代器對象,則“小掃除”會變成“大掃除”
for (; probes > 0; probes--) {
// 如果條件成立,說明當前迭代器組Itrs集合中沒有任何迭代器對象,不需要進行掃除
if (p == null) {
if (passedGo) {
break;
}
// 否則當p(開始掃除的Itrs.Node位置)爲null時,就從單向鏈表的頭節點,開始掃除
o = null;
p = head;
passedGo = true;
}
// ========== 每一次掃除處理,都會做以下操作:
// 取得當前這個Itrs.Node位置上所關聯的迭代器(注意這裏是一個弱引用)
final Itr it = p.get();
// 取得當前Itrs.Node位置的下一個Itrs.Node位置
final Node next = p.next;
// 如果條件成立,說明當前Itrs.Node需要被清理
// 那麼就是用if塊中的代碼,進行清理,並且如果發現了這種場景,掃除車就會努力的進行後續的清理
// 這就是該方法在最開始處提到的“更加努力的做後續清理”。否則,這個節點就不需要被清理
if (it == null || it.isDetached()) {
// found a discarded/exhausted iterator
// 更加努力的做後續清理,“小掃除”會變成“大掃除”,且掃除次數會被重置
probes = LONG_SWEEP_PROBES; // "try harder"
// unlink p
// 在it.isDetached()成立的場景下,主動清理這個弱引用
p.clear();
// 斷開p節點和單向鏈表後續節點的連接關係
p.next = null;
// 如果條件成立,則說明當前清掃的節點是單向鏈表的頭節點
// 那麼需要重新設定頭節點爲當前被清掃節點的後續結點。
// 如果都沒有後續結點了,那麼說明迭代器管理組中就沒有任何節點了,也就沒有必要繼續清掃下去(return退出)
if (o == null) {
head = next;
if (next == null) {
// We've run out of iterators to track; retire
itrs = null;
return;
}
}
// 其它場景下,則通過該語句將當前被清掃的p節點徹底斷開和單向鏈表各節點的關係
else {
o.next = next;
}
} else {
o = p;
}
p = next;
}
// 掃除結束後,根據最後p節點的引用情況,決定掃除車是停留在掃除結束節點上
// 還是設置爲null。核心思路是,如果當前單向鏈表的所有Itrs.Node都掃除了一次,則掃除車沒有存在的必要了
// 否則讓掃除車停留在掃除結束的位置上,以便下一次清掃請求被觸發時,繼續向後進行打掃
this.sweeper = (p == null) ? null : o;
}
通過以上方法的詳細解讀,我們知道了迭代器管理組Itrs對其中迭代器對象的清理,主要包括以下關鍵點:
- 首先根據當前sweeper掃除車的狀態,決定本次掃除的開始位置——在單向鏈表中掃除的開始位置
- 在確認完掃除開始位置後,再依次進行掃除。掃除模式分爲“小掃除”和“大掃除”。
- “小掃除”的定義是從掃除開始的位置,向後掃描最多4個節點。“小掃除”的目的在於節約操作步驟的同時,校驗鏈表中的迭代器部分正確。
- “大掃除”的定義是從掃除開始的位置,至少向後掃描16個節點。“大掃除”的目的是保證鏈表中依然有效的迭代器的管理準確性。
- “小掃除”模式可以轉變爲“大掃除”模式,既是“小掃除”過程中,發現其中一個Itrs.Node節點已經無效(爲null或者isDetached()爲true)
可以用下圖來表示doSomeSweeping(boolean)方法的主要清掃場景:
3、ArrayBlockingQueue隊列的主要構造函數
ArrayBlockingQueue隊列中一共有三個構造函數,如下所示:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// ......
/**
* 該構造函數給定一個capacity大小,以便設定ArrayBlockingQueue隊列中環形數組的最大容量
* (也就是)ArrayBlockingQueue隊列的最大容量
* 注意:如果capacity < 1,則會拋出異常
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
/**
* 該構造函數給定兩個值,進行ArrayBlockingQueue隊列的實例化
* @param capacity 當前ArrayBlockingQueue隊列的最大容量
* @param fair 是否啓用公平鎖方式,默認情況下不啓用
* @throws IllegalArgumentException if {@code capacity < 1}
*/
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隊列的實例化
* @param capacity 當前ArrayBlockingQueue隊列的最大容量
* @param fair 是否啓用公平鎖方式,默認情況下不啓用
* @param c 這是一個外部集合,這個集合不能爲null否則要報錯。
* 這些集合中的數據將會按照特定的順序被複制到ArrayBlockingQueue隊列中
*/
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 {
final Object[] items = this.items;
int i = 0;
try {
for (E e : c)
items[i++] = Objects.requireNonNull(e);
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
// ......
}
ArrayBlockingQueue隊列自身的方法,相較於其下的Itr迭代器和Itrs迭代器分組而言,就要簡單許多了。例如其三個構造函數,不需要進行逐行註釋說明,讀者就能看懂其中的意義了。
這裏只需要特別注意的是構造函數中創建的兩個condition對象,關於condition對象的詳細介紹已經在講解AQS的章節中進行了詳細說明,這裏notEmpty對象負責在ArrayBlockingQueue隊列至少有一個數據的場景下,通知可能處於阻塞狀態的消費者線程結束阻塞狀態;notFull對象作用正好相反,它負責在ArrayBlockingQueue隊列至少有一個空餘的索引位可以放入新的數據時,通知可能處於阻塞狀態的生產者線程結束阻塞狀態。(後文講解ArrayBlockingQueue隊列的具體方法時,會涉及這些過程的詳細介紹)
4、主要方法
ArrayBlockingQueue隊列實現了java.util.concurrent.BlockingQueue接口,總的來說ArrayBlockingQueue隊列中的常用方法遵循相同的處理邏輯,區別點主要在於不能正常操作時的處理方式。這裏我們選擇幾個具有代表性的操作方法進行介紹:
4.1、offer(E e) 方法
根據官方的描述,offer(E e) 方法的主要工作過程是將特定的數據添加到隊列尾部,這個數據不能爲null。如果添加操作成功,則返回true,其他情況(添加失敗)則返回false。
public boolean offer(E e) {
// 進行添加的數據對象,不能爲null
Objects.requireNonNull(e);
// 獲取隊列集合的操作權限
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 如果條件成立,說明ArrayBlockingQueue隊列集合
// 已經沒有多餘的空間進行添加操作,則返回false
if (count == items.length) {
return false;
}
// 這個else多餘了
else {
// 使用enqueue方法進行新的數據對象添加
// 最後返回true
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
4.2、put(E e) 方法
和offer(E e) 方法類似的,還有put(E e) 方法,兩者的區別是:如果ArrayBlockingQueue隊列集合不能(已經沒有多餘的空間)進行添加操作,那麼put(E e) 方法將進入阻塞狀態,直到被喚醒並能夠進行添加操作爲止。代碼片段如下:
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*/
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
// 獲取到操作權後,才能進行後續操作
lock.lockInterruptibly();
try {
// 如果條件成立,說明當前隊列中已經沒有空間進行添加操作
// 那麼進入阻塞狀態
while (count == items.length) {
notFull.await();
}
// 通過enqueue方法進行添加操作
enqueue(e);
} finally {
lock.unlock();
}
}
lock.lock()方法和lock.lockInterruptibly()方法的區別,在之前介紹AQS的文章中已經進行了說明。這裏再做一次簡單說明:lockInterruptibly()方法在獲取鎖之前會確認線程中斷信號(Thread.interrupted()),如果收到線程中斷信號,則會拋出InterruptedException 異常;而lock()方法不會考慮線程中斷信號的問題。
4.3、E take() 方法
take()方法可以從ArrayBlockingQueue隊列頭部獲取一個數據對象,如果當前ArrayBlockingQueue隊列已經沒有數據對象可以獲取,則進入阻塞狀態。該方法中實際獲取數據對象的方法,是前文中已經介紹過的dequeue()方法。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
notEmpty.await();
}
return dequeue();
} finally {
lock.unlock();
}
}
========
(ArrayBlockingQueue完,接後文《源碼閱讀(34):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(4)》)