併發佇列中迭代器弱一致性原理探究
一、前言
併發佇列裡面的Iterators是弱一致性的,next返回的是佇列某一個時間點或者建立迭代器時候的狀態的反映。當建立迭代器後,其他執行緒刪除了該元素時候並不會丟擲java.util.ConcurrentModificationException異常,能夠保持建立迭代器後的元素一定被正確的next出來。
二、 ConcurrentLinkedQueue類圖結構
以ConcurrentLinkedQueue為例說下是如何實現的,如圖內部的Itr類實現了介面Iterator的功能。 nextNode變數用來存放next()函式要返回的節點,nextItem則用於儲存next()函式要返回的節點的值,lasetRet則記錄最後一次next()時候的節點元素,用於remove操作。
三、測試程式碼
3.1 實驗一
本實驗是測試獲取迭代器後呼叫next前刪除元素看看會有什麼結果 首先列下測試程式碼: 主執行緒debug斷點檢視圖: 如圖主執行緒獲取了佇列元素zlx節點的迭代器,在呼叫next的時候debug阻塞主。 下面啟用SleepInterrupt執行緒,執行remove操作: remove後排程到主執行緒執行 可知目前佇列裡面已經沒有zlx元素了,下面看看迭代結果: zlx gh zzz 可知還是迭代出來了已經刪除的元素,並且沒丟擲異常。
3.2 試驗2
本實驗測試獲取迭代器前呼叫next後刪除迭代器後面的元素看看有什麼結果 首先列下測試程式碼: 主執行緒debug斷點圖
3.3 試驗3
本實驗測試獲取迭代器前呼叫next後刪除迭代器後面的元素看看有什麼結果 首先列下測試程式碼: 下面看看迭代結果
四、原始碼分析
首先呼叫佇列的iterator()方法時候會例項化一個迭代器,所以每次呼叫該方法都是一個新的例項,建構函式內部呼叫了advance方法,目的是確定第一個元素的iterator.
Itr() {
advance();//(1)
}
//獲取佇列中下一個可用節點。呼叫next()時候返回節點值,或者返回null
private E advance() {
//lastRet記錄呼叫最後一次呼叫next時候的節點
lastRet = nextNode;(2)
//x存放節點值
E x = nextItem;(3)
//獲取next節點
Node<E> pred, p;
//如果為nul則呼叫阻塞佇列的first方法獲取
if (nextNode == null) {
p = first();//(4)
pred = null;
} else {
//不為nul則獲取下一個節點(5)
pred = nextNode;
p = succ(nextNode);
}
for (;;) {
//p=null則直接返回,重置節點null(6)
if (p == null) {
nextNode = null;
nextItem = null;
return x;
}
//否者記錄當前節點並返回值
E item = p.item;
if (item != null) {//(7)
nextNode = p;
nextItem = item;
return x;
} else {
// 跳過null值節點(8)
Node<E> next = succ(p);
if (pred != null && next != null)
pred.casNext(p, next);
p = next;
}
}
}
//判斷是否有原始
public boolean hasNext() {
return nextNode != null;
}
//有則刪除
public E next() {
if (nextNode == null) throw new NoSuchElementException();
return advance();
}
//刪除元素
public void remove() {
Node<E> l = lastRet;
if (l == null) throw new IllegalStateException();
// rely on a future traversal to relink.
l.item = null;
lastRet = null;
}
下面看圖說話: 假設初始佇列裡面有三個元素 那麼呼叫佇列的iterator時候執行(1)(4)(7)後佇列狀態圖: 呼叫hasNext()時候知道nextNode != null所以返回true. 然後呼叫next()方法執行(2)(5)(7)後,返回zlx,佇列狀態圖 也就說第一次呼叫佇列的iterator方法會在建構函式呼叫advance方法一次,這時候已經把佇列第一個可用的節點指標賦值給nextNode,節點值賦值給nextItem;這樣當呼叫hasNext時候先看nextNode是否null,null說明佇列為空則返回false說明佇列裡面沒有元素,否者會呼叫next方法,該方法會再次呼叫advance方法,由於呼叫hasNext確定了nextNode不為null所以會呼叫(5)來獲取下次呼叫next要返回的值,也就是當前nextNode的後繼節點。如果後繼節點為null則返回nextNode對應的值nextItem,否者設定下一次呼叫next時候需要的nextNode和nextItem。 下面考慮下實驗一的情況,首先執行緒1呼叫呼叫hasNext()後情況為: 假如執行緒1呼叫next前另外執行緒把佇列裡面的zlx刪除了,現在佇列狀態: 現在在呼叫next方法(2)(5)(7)後的狀態為: 所以返回x=zlx; 試驗2的結果很明顯,這裡不再說了,下面看看試驗3 最後還有一個remove方法,他僅僅是把最後一次next時候記錄的節點內容重置為null,並且記錄節點為null,下面圖解說下: 第一次呼叫hasNext後 然後呼叫remove方法因為lastRet=null所以丟擲了異常,其實應該先呼叫next方法在呼叫remove方法。 這樣是OK的先呼叫next方法設定lastRet,然後在呼叫remove刪除。 然後看remove裡面並沒有看到有對佇列裡面的頭尾節點進行操作,也就說並沒有在佇列中移除該元素的操作,乍一看這有問題,但是沒問題:下面看刪除zlx後佇列狀態 也就是remove僅僅把節點內容變為null,所以head還是指向這個元素(注意本節講的都是不帶哨兵節點的佇列,正常情況下佇列一開始有個null的哨兵節點,如果本節考慮的話,那麼上面的圖應該有兩個null節點,一個是哨兵,一個是zlx節點變成的) 而poll時候如果節點內容為null則會繼續檢視後繼節點,所以這裡remove簡單的把節點內容變為null即可。
四、總結
併發佇列裡面的迭代器通過使用nextItem保留建立迭代器時候的節點的值,保證了在呼叫hasNext和next方法之間其他執行緒刪除該元素後還可以正常返回刪除節點的內容,並不丟擲異常,之所以說是弱一致性是因為呼叫next時候該元素已經不在佇列裡面了,但是迭代返回還可以返回。另外remove操作並沒有立刻把刪除的原始從佇列中幹掉,而是在出隊時候從佇列裡面解除,讓它變為自引用節點,等待被垃圾回收。