本篇來分析下阻塞隊列裏迭代器的實現,以ArrayBlockingQueue源碼來分析。
首先在開始前想一想,如何實現阻塞隊列的迭代器功能?
在併發下有些線程在讀,有些在改,還有些在使用迭代器遍歷,怎麼確保安全性?用獨佔鎖將這些操作隔離開,我們看 ArrayBlockingQueue 確實是這麼做的。
既然安全性得到保障那麼還有什麼問題是需要考慮的 ?
過時數據問題。假設一個線程從此時的 takeIndex 位置開始使用迭代器往後遍歷,每此調用 next 獲取下一個數據的操作都需要提前獲取鎖,有可能很長時間都獲取不到鎖,這段有其它線程在讀取本質上就是增加 takeIndex,那麼當輪到這個迭代線程執行時他就應該首先檢查下情況,是否自己接下來要讀取的內容已經失效了(即位置下標落後於此時 takeIndex值)。還有刪除數據時也要考慮到被刪除位置對多有迭代器的影響。
併發下不只一個線程在使用迭代器,每個使用者都會創建 Itr
對象,它們構成一條鏈,Itrs
管理這條鏈。
迭代器 Itr
當你調用 iterator() 方法時,創建迭代器對象 Itr;
public Iterator<E> iterator() {
return new Itr();
}
1,重要的變量
先來介紹下迭代器 Itr 中非常重要的變量:
/** Index to look for new nextItem; NONE at end */
private int cursor;
/** Element to be returned by next call to next(); null if none */
private E nextItem;
/** Index of nextItem; NONE if none, REMOVED if removed elsewhere */
private int nextIndex;
註解很清楚的解釋了三者的功能,每此 next() 調用返回的就是 nextItem 對象,而 nextIndex 指的是nextItem對象的下標位置,cursor 是 nextIndex 的下一個位置。在不斷 next 獲取元素過程中,這三者就是兩個在前一個在後的這麼往下移動着。
/** Last element returned; null if none or not detached. */
private E lastItem;
/** Index of lastItem, NONE if none, REMOVED if removed elsewhere */
private int lastRet;
lastItem 表示上一次返回的元素;lastRet 就是 lastItem 的下標。
我這裏稱 cursor,nextIndex,lastRet 爲迭代過程的三劍客,當滿足 cursor < 0 && nextIndex < 0 && lastRet < 0
時代表迭代器失效。
/** Previous value of takeIndex, or DETACHED when detached */
private int prevTakeIndex;
/** Previous value of iters.cycles */
private int prevCycles;
這兩個變量與處理過時數據問題有關。
prevTakeIndex 代表本次遍歷開始的位置,每此 next 都會進行修正;
prevCycles :Itrs 管理 Itr 鏈,它裏面有個變量 cycles 記錄 takeIndex 回到 0 位置的次數,迭代器的 prevCycles 存儲該值。
迭代器操作需要獲取獨佔鎖,得不到就得等待,這就造成了其存儲的 prevTakeIndex 與 prevCycles 可能過時,迭代器多處操作前都會通過這兩個值來判斷數據是否過時,以做相應的處理。
/** Special index value indicating "not available" or "undefined" */
private static final int NONE = -1;
/**
* Special index value indicating "removed elsewhere", that is,
* removed by some operation other than a call to this.remove().
*/
private static final int REMOVED = -2;
/** Special value for prevTakeIndex indicating "detached mode" */
private static final int DETACHED = -3;
DETACHED:專門用於preTakeIndex,isDetached方法通過其來判斷迭代器狀態
NONE:用於三個下標變量:cursor,nextIndex,lastRet;這三個下標變量用於迭代功能的實現。表明該位置數據不可用或未定義。
REMOVED:用於lastRet 與 nextIndex。表明數據過時或被刪除。
接下來結合具體實現來看:
2,初始化
Itr 初始化過程:
private class Itr implements Iterator<E> {
Itr() {
// assert lock.getHoldCount() == 0;
lastRet = NONE;
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock(); // 將對迭代器鏈的操作及變量的操作用鎖保護起來
try {
//數組爲空
if (count == 0) {
// assert itrs == null;
cursor = NONE;
nextIndex = NONE;
prevTakeIndex = DETACHED;
} else {
// 初始時prevTakeIndex等於takeIndex
// nextIndex 等於 takeIndex
// nextItem 爲takeIndex位置的元素
// cursor 爲takeIndex的後一個位置
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
prevTakeIndex = takeIndex;
nextItem = itemAt(nextIndex = takeIndex);
cursor = incCursor(takeIndex);
if (itrs == null) {
itrs = new Itrs(this);
} else {
itrs.register(this); // in this order
itrs.doSomeSweeping(false);
}
prevCycles = itrs.cycles;
// assert takeIndex >= 0;
// assert prevTakeIndex == takeIndex;
// assert nextIndex >= 0;
// assert nextItem != null;
}
} finally {
lock.unlock();
}
}
可以看出初始化幹了三件事:將自身加入迭代器鏈 , 初始化變量 , 清掃迭代器鏈。
2.1,初始化變量
- prevTakeIndex等於 takeIndex,記錄本次迭代開始的位置,每此next都會對其進行更新。
- prevCycles = itrs.cycles,用於判斷接下來數據是否過時。
- nextIndex 等於 takeIndex,這三個變量都是迭代過程中使用到的。
- nextItem 爲takeIndex位置的元素
- cursor 爲takeIndex的後一個位置
關於 cursor 的增加操作
private int incCursor(int index) {
// assert lock.getHoldCount() == 1;
if (++index == items.length)
index = 0;
if (index == putIndex)
index = NONE;
return index;
}
當遍歷到 putIndex ,代表數據遍歷結束,應該終止迭代,將 cursor 置爲 NONE 標識,cursor 置爲 NONE 後會引起迭代器的終止,邏輯在 next 與 hasNext 方法中。
2.2,註冊
private Node head; // 標識迭代器鏈的頭節點
void register(Itr itr) {
// assert lock.getHoldCount() == 1;
head = new Node(itr, head);
}
private class Node extends WeakReference<Itr> {
Node next;
Node(Itr iterator, Node next) {
super(iterator);
this.next = next;
}
}
將Node對 Itr
對象的引用設置爲弱引用。當迭代線程結束迭代後就只會剩下Node 對 Itr 的弱引用,當 GC 啓動後會回收該對象,通過 get 的返回來判斷是否被回收。對於迭代器對象被回收的節點會被從迭代器鏈中刪除。
2.3,清掃迭代器鏈 doSomeSweeping
清掃方法 doSomeSweeping 並非一次將整個鏈條探測一遍,開始時選擇探測的範圍4或16,若在範圍內未探測到無效迭代器則結束,若是探測到則擴大探測範圍,將範圍恢復爲16,繼續往下探測。
先來介紹 Itrs
中的三個相關變量
//記錄上次探測的結束位置節點,下次從此開始往後探測。
private Node sweeper = null;
// 探測範圍
private static final int SHORT_SWEEP_PROBES = 4;
private static final int LONG_SWEEP_PROBES = 16;
doSomeSweeping 將清除迭代器鏈中的無效節點,所謂無效指的是:1,節點持有的 Itr
對象爲空,說明被GC回收,能被回收也說明使用它的線程完成了迭代。2,迭代器 Itr
此時是 DETACHED 模式,對於迭代結束的迭代器會被置於 DETACHED 模式。迭代結束指的是迭代到 cursor 等於 putIndex,標誌着全部數據迭代完畢。
doSomeSweeping 清掃迭代鏈但並非從頭到尾全部探測一遍。之所以有這種設計我想是爲了避免耗時,畢竟此時迭代線程持有着獨佔鎖。
void doSomeSweeping(boolean tryHarder) {
// assert lock.getHoldCount() == 1;
// assert head != null;
//tryHarder 爲true則 probes 爲 16,否則爲 4
// probes 代表本次的探測長度,所以你明白爲啥取名叫tryHader了
int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
Node o, p; // o 代表 p 的前一個節點,用於鏈表中節點的刪除
// 它代表了一次探測中到達的最後一個節點
final Node sweeper = this.sweeper;
// 限制最多隻遍歷一遍
// 若本次遍歷從半道開始不是從頭開始,當其遍歷到尾部但probes仍>0
// 則轉到頭節點繼續;但若是從頭開始的遍歷,遍歷到尾probes>0,沒必要再繼續,應該終止遍歷。
// 該功能的實現靠的就是 passedGo
boolean passedGo;
// 從頭開始的遍歷,passedGo 設爲true,在遇到鏈表長度小於probes的情況,能夠break終止
if (sweeper == null) {
o = null;
p = head;
passedGo = true;
// 從前一個線程終止的位置開始
} else {
o = sweeper;
p = o.next;
passedGo = false;
}
for (; probes > 0; probes--) {
if (p == null) {
if (passedGo) // 這就是passedGo發揮作用的地方
break;
//passedGo爲false,說明本次遍歷是從中間某位置開始,
//也就是說鏈表前面有一段是未遍歷的,
//遍歷到了尾部需要轉回到頭部繼續遍歷
o = null;
p = head;
passedGo = true;
}
final Itr it = p.get();
final Node next = p.next;
//節點持有的迭代器對象爲null,或是數組爲空或數據過時導致的DETACHED模式,
//則刪除此節點
if (it == null || it.isDetached()) {
//當發現了一個被拋棄或過時的迭代器,
//則將探測範圍probes變爲16,相當於延長探測範圍。
//這就是探測的本意吧:找不到就按原先的探測範圍直到結束,
//若找到了一個就擴大探測範圍繼續往下找。
//以上的設計可能原因是:無論是由於 被回收 或是 過時
// 發現一個則之後的節點都極有可能存在相同的情況
probes = LONG_SWEEP_PROBES; // "try harder"
// unlink p
p.clear();
p.next = null;
if (o == null) {
head = next;
if (next == null) {
// We've run out of iterators to track; retire
itrs = null;
return;
}
}
else
o.next = next;
} else {
o = p;
}
p = next;
}
// 記錄本次遍歷結束位置節點
this.sweeper = (p == null) ? null : o;
}
3,迭代操作hasNext與next
3.1,hasNext
public boolean hasNext() {
// assert lock.getHoldCount() == 0;
if (nextItem != null)
return true;
noNext();
return false;
}
nextItem != null 說明仍有元素可以繼續遍歷,nextItem 代表next方法的返回值。當沒有元素可繼續遍歷時調用 noNext 方法。
noNext
隨着 next
的不斷調用,cursor 隨之增加,最後 cursor 等於 putIndex,表明數組元素全部遍歷完,這之後下標變量變爲 cursor = NONE ,nextIndex = NONE , nextItem = null
,邏輯再next
方法中。到這裏直接退出不就行了 ? 不行,需要將迭代器的狀態置爲 DETACHED,這樣才能被 doSomeSweeping
方法清除。這就是 noNext
方法實現的功能:將迭代器狀態置爲 DETACHED。
private void noNext() {
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
try {
//調用該方法時cursor與nextIndex皆爲NONE,邏輯在next方法中
// assert cursor == NONE;
// assert nextIndex == NONE;
// detach可能在最後一次next中被調用,所以這裏先進行判斷
if (!isDetached()) {
//lastRet記錄前一個next返回的元素的下標,
//cursor等於putIndex,迭代結束,方法運行到此
//此時的lastRet一定是>= 0的
// assert lastRet >= 0;
//該方法處理數據過時問題,會修正下標變量或直接detach該迭代器
//那麼爲什麼在這裏調用該方法,有必要嗎?
//因爲我們要對 lastItem 進行賦值,可若數據過時了便沒有這個必要了
//所以這裏調用該方法,若過時則lastRet被置爲REMOVED小於0,還會調用detach方法
incorporateDequeues(); // might update lastRet
if (lastRet >= 0) {
lastItem = itemAt(lastRet);
// assert lastItem != null;
detach(); // 調用detach
}
}
// assert isDetached();迭代器處於了DETACHED狀態
// assert lastRet < 0 ^ lastItem != null;相當於lastRet < 0 && lastItem == null
} finally {
lock.unlock();
}
}
首先代碼的執行是需要先獲取鎖的。
從代碼中可以看出detach
一定會被調用,隱藏在incorporateDequeues中或是直接調用detach。還有需要注意的是代碼中聲明的幾個assert,理解他們有助於理解代碼的設計。
3.2,next
public E next() {
// assert lock.getHoldCount() == 0;
final E x = nextItem;
if (x == null)
throw new NoSuchElementException();
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
try {
if (!isDetached())
//修正下標,主要是cursor,確保其在當前takeIndex之後(包括等於takeIndex)
//從而保證返回的元素不是過時的數據
incorporateDequeues();
// assert nextIndex != NONE;
// assert lastItem == null;
lastRet = nextIndex;
final int cursor = this.cursor;
if (cursor >= 0) {
nextItem = itemAt(nextIndex = cursor);
// assert nextItem != null;
this.cursor = incCursor(cursor);
} else {
nextIndex = NONE;
nextItem = null;
}
} finally {
lock.unlock();
}
return x;
}
重要的有兩點:1,需要獲取鎖。2,獲取前修正相關下標變量的值。用獨佔鎖保證了安全,可也造成了獲取元素前檢查數組狀態的必要性。
incorporateDequeues方法
該方法主要作用是:修正下標,保證返回數據的有效性。
由於多線程下爲了確保安全迭代線程每次next都要先獲取獨佔鎖,得不到便需等待,等到被喚醒繼續執行就需要對數組此時的狀況進行判斷,判斷當前迭代器要獲取的數據是否已經過時,將最新的 takeIndex 賦給迭代器的 cursor,從而確保迭代器不會返回過時的數據。
private void incorporateDequeues() {
// assert lock.getHoldCount() == 1;
// assert itrs != null;
// assert !isDetached();
// assert count > 0;
final int cycles = itrs.cycles;
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
final int prevCycles = this.prevCycles;
final int prevTakeIndex = this.prevTakeIndex;
if (cycles != prevCycles || takeIndex != prevTakeIndex) {
final int len = items.length;
// how far takeIndex has advanced since the previous
// operation of this iterator
//計算此時takeIndex 與 迭代器存儲的prevTakeIndex之間的長度
//接下來要用它來判斷迭代器接下來讀取的數據是否已過時
long dequeues = (cycles - prevCycles) * len
+ (takeIndex - prevTakeIndex);
//接下來就是檢查各個下標變量lastRet,nextIndex,cursor
//查看它們指向的數據是否已過時,所謂過時指的是此時 takeIndex 已在其前
//若過時就將lastRet與nextIndex置爲REMOVED,cursor置爲此時的takeIndex
if (invalidated(lastRet, prevTakeIndex, dequeues, len))
lastRet = REMOVED;
if (invalidated(nextIndex, prevTakeIndex, dequeues, len))
nextIndex = REMOVED;
if (invalidated(cursor, prevTakeIndex, dequeues, len))
cursor = takeIndex;
// 這三個下標變量若都<0,說明該終止此迭代器
// detach會將preTakeIndex置爲DETACHED,然後調用doSomeSweeping清掃迭代器鏈
//在isDetached中就是通過preTakeIndex是否小於0來判斷迭代器是否終止
if (cursor < 0 && nextIndex < 0 && lastRet < 0)
detach();
//迭代器沒有作廢的話,更新prevCycles與prevTakeIndex的值
//回到next方法從takeIndex處開始繼續往下遍歷
else {
this.prevCycles = cycles;
this.prevTakeIndex = takeIndex;
}
}
}
invalidated 返回true 說明 index 失效。失效說明該下標變量在 takeIndex 之前。
private boolean invalidated(int index, int prevTakeIndex,
long dequeues, int length) {
// 下標變量小於0返回false,表明該下標變量的值不需要進行更改
// 有三個狀態 NONE ,REMOVED,DETACHED 它們皆小於0。
//DETACHED:專門用於preTakeIndex使用,isDetached方法通過其來判斷
//NONE:用於三個下標變量:cursor,nextIndex,lastRet;這三個用於迭代功能的實現。
//它們爲NONE,表明迭代結束可能因爲數組爲空或是遍歷完。
//REMOVED:用於lastRet 與 nextIndex。表面數據過時。
if (index < 0)
return false;
int distance = index - prevTakeIndex;
if (distance < 0)
distance += length;
return dequeues > distance;
}
detach
private void detach() {
// Switch to detached mode 將迭代器轉換爲 detached模式
// 下面的這些申明都說明了調用該方法時數組的狀態
// assert lock.getHoldCount() == 1; 持有鎖
// 下面四個下標變量的狀態都表明迭代器
// assert cursor == NONE;
// assert nextIndex < 0;
// assert lastRet < 0 || nextItem == null;
// assert lastRet < 0 ^ lastItem != null;相當於lastRet < 0 && lastItem == null
if (prevTakeIndex >= 0) {
// assert itrs != null;
prevTakeIndex = DETACHED;
// try to unlink from itrs (but not too hard)
itrs.doSomeSweeping(true);
}
}
重點在於最後的四個 assert 聲明:
- cursor == NONE
- nextIndex < 0;
- lastRet < 0 || nextItem == null
- lastRet < 0 ^ lastItem != null 相當於lastRet < 0 && lastItem == null
這便是迭代器的 DETACHED 狀態,之後會將prevTakeIndex = DETACHED 用來標識該迭代器此時處於DETACHED 狀態, isDetached 方法會返回true,從而在清掃方法 doSomeSweeping 中將該節點刪除。
detach 方法代碼中做了兩件事:1,prevTakeIndex = DETACHED; 在 isDetached 中便是通過 prevTakeIndex 值是否小於0來判斷迭代器是否處於 DETACHED 狀態。2,調用 doSomeSweeping 刪除該節點,並清掃迭代鏈,注意這裏傳的是true,也就是以最大探測範圍 16 開始探測迭代鏈。爲什麼傳true?爲了儘可能刪除該節點。
這裏有個問題:detach 裏調用 doSomeSweeping 一定能刪除掉該節點嗎?不一定,該方法在上面分析過,它的清掃是從上一個線程清掃結束的地方開始,探測範圍爲4或16,若在範圍內沒探測到就結束,若探測到就擴大探測範圍,範圍恢復爲16再繼續往下探測。