(接上文《源碼閱讀(32):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(2)》)
2.3.3、迭代器中主要方法分析
一旦Itr迭代器完成初始化,就可以開始使用了。而使用迭代器最常見的方法就是使用hasNext()方法和next()方法進行配合。另外從JDK 1.8+開始,還可以使用Lambda表達式進行表達,最後ArrayBlockingQueue隊列集合的迭代器還支持remove()方法的使用。
2.3.3.1、hasNext()方法和next()方法
我們先行來介紹hasNext()方法和next()方法,這也是最常見的迭代器使用方法,從JDK 1.8開始,java.util.Iterator定義了另一種方法void forEachRemaining(Consumer<? super E> action),其默認實現也是基於hasNext()方法和next()方法的配合使用,如下所示:
public interface Iterator<E> {
// ......
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
// hasNext()方法和next()方法進行配合,構成了默認的處理方式
while (hasNext()) {
action.accept(next());
}
}
// ......
}
當然由於ArrayBlockingQueue隊列特殊的內部結構,其對forEachRemaining()方法進行了重寫(本文的後續部分會進行介紹),下面我們再來看看最常見的hasNext()方法和next()方法配合使用的方式,代碼片段如下所示:
// ......
Iterator<String> itr = queue.iterator();
while(itr.hasNext()) {
System.out.println(itr.next());
}
// ......
2.3.3.1.1、最簡單的運行情況
當然,hasNext()方法和next()方法在配合使用的過程中有許多處理分支,所以,爲了方便讀者理解,我們介紹這組方法時會按照這組方法最常見的運行方式去進行講解,首先我們來看一下hasNext()方法的部分代碼,如下所示:
// hashNext方法,如果迭代器還可以遍歷下一個數據,則返回true;其它情況返回false
public boolean hasNext() {
if (nextItem != null)
return true;
// ...... hasNext並沒有全部展示,我們先將注意力集中在以上條件成立的情況
}
爲了便於理解以下的代碼,特別是Itr迭代器中nextItem變量的引用情況,本文這裏放置一張上文已經提到的ArrayBlockingQueue隊列初始化後的狀態圖:
通常情況下,hasNext()方法儘可能保證自己不會在獲得操作鎖以後才能進行工作,這樣做是爲了儘可能提高併發操作性能。爲了達到這個目的hasNext方法通常以nextItem屬性的值狀態作爲判定依據。並且只要迭代器還沒有完成遍歷,nextItem屬性中的一定都會有值。如果hasNext()方法返回true,一般情況下使用者就可以使用next()方法拿取這個遍歷點上的數據,代碼片段如下所示:
public E next() {
final E e = nextItem;
// 注意,如果nextItem中沒有數據,則直接拋出異常,這就是爲什麼在執行next()方法前,
// 一定要先使用hasNext()方法檢查迭代器的有效性
if (e == null)
throw new NoSuchElementException();
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
// 只有在獲得鎖的情況下才能執行next遍歷操作
lock.lock();
try {
// 如果當前迭代器不是“獨立”模式(也就是說沒有失效)
// 則通過incorporateDequeues方法對lastRet、nextIndex、cursor、prevCycles、prevTakeIndex屬性進行修正
// 保證以上這些屬性的狀態值,和當前ArrayBlockingQueue隊列集合的狀態一致。
// incorporateDequeues方法很重要,下文中立即會進行介紹
if (!isDetached()) {
incorporateDequeues();
}
// assert nextIndex != NONE;
// assert lastItem == null;
// 將nextIndex索引位置賦值給lastRet,表示之前取next元素的索引位已經變成了“上一個”取出數據的索引位
lastRet = nextIndex;
final int cursor = this.cursor;
// 如果當前遊標有效(不爲NONE)
if (cursor >= 0) {
// 那麼遊標的索引位置就成爲下一個取數的位置
nextItem = itemAt(nextIndex = cursor);
// assert nextItem != null;
// 接着遊標索引位+1,注意:這是遊標索引位可能爲None
// 代表取出下一個數後,就再無數可取,遍歷結束
this.cursor = incCursor(cursor);
}
// 否則就認爲已無數可取,迭代器工作結束
else {
// 這時設定nextIndex爲NONE,,設定nextItem爲Null
nextIndex = NONE;
nextItem = null;
// 如果條件成立,則標識當前迭代器爲“獨立”(無效)工作狀態
if (lastRet == REMOVED) {
detach();
}
}
} finally {
lock.unlock();
}
return e;
}
// 該方法負責將當前Itr迭代器置爲“獨立/失效”工作狀態,既將prevTakeIndex設置爲DETACHED
// 這個動作可能發生在以下多種場景下:
// 1、當Itrs迭代器組要停止對某個Itr迭代器進行狀態跟蹤時。
// 2、當迭代器中已經沒有更多的索引位可以遍歷時。
// 3、當迭代器發生了一些處理異常時,
// 4、當incorporateDequeues()方法中判定三個關鍵索引位全部失效時(cursor < 0 && nextIndex < 0 && lastRet < 0)
// 5、.....
private void detach() {
// Switch to detached mode
// assert lock.isHeldByCurrentThread();
// assert cursor == NONE;
// assert nextIndex < 0;
// assert lastRet < 0 || nextItem == null;
// assert lastRet < 0 ^ lastItem != null;
if (prevTakeIndex >= 0) {
// assert itrs != null;
// 設定一個Itr迭代器失效,就是設定prevTakeIndex屬性爲DETACHED常量
prevTakeIndex = DETACHED;
// try to unlink from itrs (but not too hard)
// 一旦該迭代器被標識爲“獨立”(無效)工作模式,則試圖清理該迭代器對象在Itrs迭代器組中的監控信息
itrs.doSomeSweeping(true);
}
}
以上代碼片段,如果在ArrayBlockingQueue隊列集合有數據的場景下,在迭代器剛完成初始化後第一次運行時,通常來說運行結果類似如下所示:
2.3.3.1.2、考慮多線程介入操作的情況
但實際情況可能要複雜得多,因爲ArrayBlockingQueue隊列被設計成能在多線程同時操作的場景下正確工作,所以有可能在兩次next()方法操作之間很短的時間內,ArrayBlockingQueue隊列已經被其它線程進行了多次讀寫操作。甚至ArrayBlockingQueue隊列中關鍵的takeIndex參數已在環形數組中循環多次(跨越0號索引位多次),如下圖所示:
如上圖所示,在完成迭代器的next()操作後,ArrayBlockingQueue隊列的操作權就被釋放(lock.unlock()),這時其它線程操作ArrayBlockingQueue隊列做了很多讀/寫操作,導致ArrayBlockingQueue隊列的putIndex索引位置跨過0號索引位1次(iters.cycles值爲1),並且停留在1號索引位的位置。而且由於這些讀操作,目前1號索引位、2號索引位、3號索引位上已經沒有數據,Itr迭代器還沒有開始遍歷到的2號索引位之後的數據也已經全部更換了一批。
需要注意的是,以上場景中我們不考慮(iters.cycles - prevCycles > 1)的情況,因爲這種情況迭代器內部已經做了安全限定。以上情況看似很極端,但實則在多線程場景下是比較常見的,在這樣的情況下,Itr迭代的的hasNext()/next()方法又開始被調用了(連續進行了多次調用):
public boolean hasNext() {
// 由於nextItem已經在上次next()方法中,提前引用了最初存儲在2號索引位上的數據
// 所以這時的判定條件返回的還是true
if (nextItem != null)
return true;
noNext();
return false;
}
由於以上hasNext()方法返回了true,所以調用者依然可以合法調用next()方法,代碼片段如下所示(一些重複且不會涉及的代碼片段就進行省略了):
//......
public E next() {
// ......
try {
if (!isDetached()) {
// 進行有出隊數據存在情況下的索引位置判定和調整
// 我們主要就來分析,該方法如何進行索引位置修正
incorporateDequeues();
}
// ......
} finally {
lock.unlock();
}
return e;
}
//......
// 該方法用於判定當前迭代器是否採用“獨立”模式在運行
boolean isDetached() {
// assert lock.isHeldByCurrentThread();
return prevTakeIndex < 0;
}
// 該方法在用於在Itr迭代器多次操作的間歇間,ArrayBlockingQueue隊列狀態發生變化的情況下
// 對Itr的重要索引位置進行修正(甚至是讓Itr在極端情況下無效)
private void incorporateDequeues() {
// assert lock.isHeldByCurrentThread();
// assert itrs != null;
// assert !isDetached();
// assert count > 0;
// 這是ArrayBlockingQueue目前記錄的takeIndex索引位回到0號索引位的次數
final int cycles = itrs.cycles;
// 這是ArrayBlockingQueue目前記錄的takeIndex索引位的值
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
// 這是本迭代器中上一次獲取到的takeIndex索引位回到0號索引位的次數(值爲0)
final int prevCycles = this.prevCycles;
// 這是本迭代器中上一次獲取到的takeIndex索引位的值
final int prevTakeIndex = this.prevTakeIndex;
// 如果發現cycles和prevCycles存在差異,或者takeIndex和prevTakeIndex存在差異
// 則說明在迭代器的兩次操作間隔中,ArrayBlockingQueue中的數據發生了變化,那麼需要進行修正
if (cycles != prevCycles || takeIndex != prevTakeIndex) {
// ArrayBlockingQueue隊列中循環數組的容量長度
// 和代碼配套的示意圖中,該值爲X+1
final int len = items.length;
// how far takeIndex has advanced since the previous
// operation of this iterator
// 這句計算非常重要,就是計算在所有讀取操作後,兩次takeIndex索引產生的索引距離(已出隊的數據量)
long dequeues = (long) (cycles - prevCycles) * len + (takeIndex - prevTakeIndex);
// Check indices for invalidation
// 判定lastRet索引位置是否失效,如果失效則賦值爲-2
if (invalidated(lastRet, prevTakeIndex, dequeues, len)) {
lastRet = REMOVED;
}
// 判定nextIndex索引位是否失效,如果失效則賦值爲-2
if (invalidated(nextIndex, prevTakeIndex, dequeues, len)) {
nextIndex = REMOVED;
}
// 判定nextIndex索引位是否失效,如果失效則將ArrayBlockingQueue目前記錄的takeIndex索引位的值賦給它
// 讓cursor遊標索引位,指向當前ArrayBlockingQueue隊列的head位置
if (invalidated(cursor, prevTakeIndex, dequeues, len)) {
cursor = takeIndex;
}
// 如果cursor索引、nextIndex索引、lastRet索引,則表示當前Itr遊標失效
// 調用detach()方法將當前Itr迭代器標記爲失效,並清理Itrs迭代器組中的Node信息
if (cursor < 0 && nextIndex < 0 && lastRet < 0) {
detach();
}
// 否則(大部分情況)修正Itr迭代器中的狀態,以便其能從修正的位置開始進行遍歷
else {
this.prevCycles = cycles;
this.prevTakeIndex = takeIndex;
}
}
}
/**
* Returns true if index is invalidated by the given number of
* dequeues, starting from prevTakeIndex.
* 該方法依據prevTakeIndex索引的位置、兩次takeIndex索引移動的距離(已出隊的數據量),以便判定給定的index索引位置是否已經失效
* 如果失效,則返回true,其它情況返回false。
*/
private boolean invalidated(int index, int prevTakeIndex, long dequeues, int length) {
// 如果需要判定的索引位本來就已經失效了(NONE、REMOVED、DETACHED這些常量都爲負數)
if (index < 0) {
return false;
}
// 計算index索引位置和prevTakeIndex索引位置的距離
// 最簡單的就是當前index的索引位減去prevTakeIndex的索引位值
int distance = index - prevTakeIndex;
// 如果以上計算出來是一個負值,說明index的索引位已經“繞場一週”
// 這時在distance的基礎上面,增加一個隊列長度值,
if (distance < 0) {
distance += length;
}
return dequeues > distance;
}
以上代碼片段中incorporateDequeues()方法非常重要,該方法中有一句代碼又是最關鍵:
long dequeues = (long) (cycles - prevCycles) * len + (takeIndex - prevTakeIndex);
這句代碼主要是爲了檢測在Itr迭代器兩次操作的間隔中,ArrayBlockingQueue被其它線程進行讀操作,使得ArrayBlockingQueue隊列中takeIndex索引位置移動的真實距離,如下圖所示:
上圖中將ArrayBlockingQueue隊列的環形數組結構進行了平展,示意了ArrayBlockingQueue隊列進行若干讀寫操作後,takeIndex索引位置移動的距離,這個距離將作爲後續invalidated(int, int , long , int)方法中判定指定的索引位置是否已失效的重要依據。我們將示例場景帶入以上代碼中的各個計算公式——來看看ArrayBlockingQueue隊列被操作後的各種屬性的變化:
-
cycles的值爲1 ——因爲takeIndex索引回到0號索引位的次數總共爲1,prevCycles的值爲0——因爲Itr迭代器上一次獲取到的cycles值爲0;len長度爲X + 1;takeIndex索引位的值爲3;prevTakeIndex索引位的值爲1,所以最終得到的dequeues變量的值爲(X+1)+ 2。
-
當進行“invalidated(lastRet, prevTakeIndex, dequeues, len)”調用時:lastRet索引位的值爲1,prevTakeIndex的值爲1,所以計算出來的distance的值爲0;那麼 “dequeues > distance”的判定將會返回true。所以lastRet索引被標識爲-2(已移除)。
-
當進行“invalidated(nextIndex, prevTakeIndex, dequeues, len)”調用時:nextIndex索引位的值爲2,prevTakeIndex的值也爲1,所以計算出來的distance的值爲1,那麼 “dequeues > distance”的判定將會返回true。所以nextIndex索引被標識爲-2(已移除)。
-
當進行“invalidated(cursor, prevTakeIndex, dequeues, len)”調用時:cursor索引位的值爲3,prevTakeIndex的值還是爲1,所以計算出來的distance的值爲2,那麼 “dequeues > distance”的判定將會返回true。所以cursor 索引被標識爲ArrayBlockingQueue目前記錄的takeIndex索引位的值。
-
請注意:incorporateDequeues()方法的目標是在Itr迭代器兩次操作間隙ArrayBlockingQueue隊列發生讀寫操作的情況下,儘可能修正Itr迭代器的索引位值,使它能從下一個正確的索引位置重新開始遍歷數據,而不是“儘可能讓Itr迭代器作廢”。這從incorporateDequeues()方法中確認Itr迭代器過期所使用的相對苛刻的判定條件就可以看出來 “cursor < 0 && nextIndex < 0 && lastRet < 0”。
-
請特別注意以上條件中的“cursor < 0” 這個條件因子,只要ArrayBlockingQueue隊列中增加了之前沒有遍歷過的元素,且cursor索引值本身並不爲NONE;或者ArrayBlockingQueue隊列中還有遺留的沒有遍歷過,沒有出隊的數據,則cursor索引值都會被修正成一個非負數。
爲了再爲讀者進行更多處理情況的說明,這裏我們給出更多的場景示意:
incorporateDequeues()方法非常重要還有一個原因,是Itr迭代器內部隱含的一個處理邏輯:既獲得ArrayBlockingQueue隊列操作權的Itr迭代器,在進行正式操作前,都必須使用incorporateDequeues()方法修正Itr中關鍵的索引信息(lastRet、nextIndex、cursor)以保證Itr迭代器在多線程併發讀/寫ArrayBlockingQueue隊列的操作場景下索引位置的正確性。
2.3.3.1.3、當hasNext()方法發現沒有更多數據可遍歷時
根據以上介紹的hasNext()方法/next()方法在一般工作場景下的配合使用,以及next()方法中對遍歷索引位的修正工作原理,存儲在ArrayBlockingQueue隊列中的所有數據就可以被遍歷了,直到hasNext()方法返回false,告訴調用者再無數據可進行遍歷未知,如下所示:
public boolean hasNext() {
if (nextItem != null)
return true;
// 如果再沒有數據可以遍歷,則調用noNext()方法後,返回false
noNext();
return false;
}
// 該方法主要在再沒有數據可以遍歷的情況下,修正和設定當前Itr迭代器的狀態。
private void noNext() {
// 首先該方法需要獲得ArrayBlockingQueue隊列的操作權限
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
try {
// assert cursor == NONE;
// assert nextIndex == NONE;
// 如果當前Itr迭代器並沒有被設定爲“獨立”(失效)工作模式,則需要進行狀態設定
if (!isDetached()) {
// assert lastRet >= 0;
// 首先修正lastRet、nextIndex、cursor三個關鍵索引值
// incorporateDequeues()方法非常重要,上文已經進行了說明
incorporateDequeues(); // might update lastRet
// 如果代表最後一次(上一次)next()方法返回數據所在索引位的lastRet索引值有效
// 則還要視圖取出這個索引位上的數據。
if (lastRet >= 0) {
lastItem = itemAt(lastRet);
// assert lastItem != null;
// 設定當前Itr迭代器失效,並清理Itrs迭代器組中的Node信息
detach();
}
}
// assert isDetached();
// assert lastRet < 0 ^ lastItem != null;
} finally {
lock.unlock();
}
}
那麼有的讀者會有這個疑問,爲什麼當迭代器沒有任何數據可以遍歷的時候,還要通過incorporateDequeues()方法修正各索引位的值,並且還要視圖在取出lastRet索引位上的數據後,才設定迭代器失效呢?爲什麼不是直接設定Itr迭代器失效就可以了呢?這個原因和remove()方法的處理邏輯有關係。
2.3.3.2、remove()方法
remove()方法的作用是刪除Itr迭代器上一次從next()方法獲取數據時,其索引位上的數據(lastRet索引位上的數據)——真正的從ArrayBlockingQueue隊列中刪除。一定要注意不是刪除當前cursor遊標指向的索引位上的數據。
雖然我們在使用ArrayBlockingQueue隊列的Itr迭代器時,不會習慣於使用其中的remove()方法,從ArrayBlockingQueue隊列中移除上次調用next()方法時返回的索引位上的數據信息;雖然java.util.Iterator接口默認爲該接口的各種迭代器實現類不支持remove()方法,但確實ArrayBlockingQueue隊列的Itr迭代器支持調用者有限使用remove()方法。所以我們還是要需要進行介紹,作爲對上文中已詳細介紹的hasNext()方法、next()方法的補充:
// java.util.Iterator接口默認爲該接口的實現不支持remove()方法
public interface Iterator<E> {
// ......
default void remove() {
throw new UnsupportedOperationException("remove");
}
// ......
}
以下是remove()方法的代碼片段:
private class Itr implements Iterator<E> {
// ......
// remove方法的操作意義並不是移除當前cursor遊標所指向的索引位上的數據
// 而是移除上一次通過next()方法返回的索引位上的數據,也就是當前lastRet所指向的索引位上的數據
public void remove() {
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
// assert lock.getHoldCount() == 1;
try {
// 同樣,獲取操作權後,首先通過incorporateDequeues()方法
if (!isDetached()) {
incorporateDequeues(); // might update lastRet or detach
}
// 設定Itr中的全局lastRet變量爲NONE,以表示該位置的數據將被移除
// 設定前,將該值存儲到局部變量中,以便後續使用
final int lastRet = this.lastRet;
this.lastRet = NONE;
if (lastRet >= 0) {
// 如果lastRet的索引位有效,且Itr迭代器有效,則移除ArrayBlockingQueue隊列中lastRet索引位上的數據
if (!isDetached()) {
removeAt(lastRet);
}
// 如果lastRet的索引位有效,但Itr迭代器無效,
// 則移除ArrayBlockingQueue隊列中lastRet索引位上的數據
// 還要取消lastItem對數據對象的引用
else {
final E lastItem = this.lastItem;
// assert lastItem != null;
this.lastItem = null;
if (itemAt(lastRet) == lastItem) {
removeAt(lastRet);
}
}
}
// 如果lastRet已被標識爲無效
// 出現這種情況的場景最有可能是Itr迭代器創建時ArrayBlockingQueue隊列中沒有任何數據
// 或者是Itr迭代器創建後,雖然有數據可以遍歷,但是還沒有使用next()方法讀取任何索引位上的數據
// 這是拋出IllegalStateException異常
else if (lastRet == NONE) {
throw new IllegalStateException();
}
// else lastRet == REMOVED and the last returned element was
// previously asynchronously removed via an operation other
// than this.remove(), so nothing to do.
// 以上這段註釋是源代碼中Doug Lea書寫的註釋,大意是說
// 如果lastRet已被標識爲“已移除”(REMOVED)狀態
// 說明這個索引位上數據已經被之前獲得操作權的其它操作線程移除隊列,所以這裏無需做任何處理了
// 最後,如果以下條件成立,則標識當前Itr迭代器無效
if (cursor < 0 && nextIndex < 0) {
detach();
}
} finally {
lock.unlock();
// assert lastRet == NONE;
// assert lastItem == null;
}
}
// ......
}
關於ArrayBlockingQueue隊列的removeAt(int)方法,我們將在下文中進行介紹。現在我們對本小節開始時的兩個問題進行解答:
- 爲什麼在noNext()方法開始處理前、remove()方法開始處理前都要通過incorporateDequeues()方法修正各索引位的值?
這就是上文中已經提到的Itr迭代器隱含的處理規則。因爲ArrayBlockingQueue、ArrayBlockingQueue.itr的設計者始終考慮的是前者的工作場景是被多線程併發操作的,在本Itr迭代器獲取到操作權之前,不能保證當前Itr迭代器的關鍵索引位置都處於正確的值。所以都需要通過incorporateDequeues()方法進行修正。
- 爲什麼noNext()方法處理過程中,還要試圖在取出lastRet索引位上的數據後,才設定迭代器失效?
這是因爲雖然最後一次有效使用next()方法遍歷了ArrayBlockingQueue隊列中的最後一個數據,但是還要考慮支持操作者這時通過remove()方法刪除這個最後索引位上的數據,所以需要在hasNext()方法中將這個索引位上的數據取出並引用起來,以便在進行remove操作時正確刪除數據。
========
(接後文《源碼閱讀(34):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(4)》)