1. 程式人生 > >併發佇列中迭代器弱一致性原理探究

併發佇列中迭代器弱一致性原理探究

一、前言

併發佇列裡面的Iterators是弱一致性的,next返回的是佇列某一個時間點或者建立迭代器時候的狀態的反映。當建立迭代器後,其他執行緒刪除了該元素時候並不會丟擲java.util.ConcurrentModificationException異常,能夠保持建立迭代器後的元素一定被正確的next出來。

二、 ConcurrentLinkedQueue類圖結構

image.png 以ConcurrentLinkedQueue為例說下是如何實現的,如圖內部的Itr類實現了介面Iterator的功能。 nextNode變數用來存放next()函式要返回的節點,nextItem則用於儲存next()函式要返回的節點的值,lasetRet則記錄最後一次next()時候的節點元素,用於remove操作。

三、測試程式碼

3.1 實驗一

本實驗是測試獲取迭代器後呼叫next前刪除元素看看會有什麼結果 首先列下測試程式碼: image.png 主執行緒debug斷點檢視圖: image.png 如圖主執行緒獲取了佇列元素zlx節點的迭代器,在呼叫next的時候debug阻塞主。 下面啟用SleepInterrupt執行緒,執行remove操作: image.png remove後排程到主執行緒執行 image.png 可知目前佇列裡面已經沒有zlx元素了,下面看看迭代結果: zlx gh zzz 可知還是迭代出來了已經刪除的元素,並且沒丟擲異常。

3.2 試驗2

本實驗測試獲取迭代器前呼叫next後刪除迭代器後面的元素看看有什麼結果 首先列下測試程式碼: image.png 主執行緒debug斷點圖 image.png

如圖主執行緒獲取了佇列元素zlx節點的迭代器,在呼叫next的時候debug阻塞主。 下面啟用SleepInterrupt執行緒,執行remove操作: image.png remove後排程到主執行緒執行 image.png 可知現在佇列裡面沒有了gh元素,下面看看迭代結果 zlx zzz

3.3 試驗3

本實驗測試獲取迭代器前呼叫next後刪除迭代器後面的元素看看有什麼結果 首先列下測試程式碼: image.png 下面看看迭代結果 image.png

四、原始碼分析

首先呼叫佇列的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;
}

下面看圖說話: 假設初始佇列裡面有三個元素 image.png 那麼呼叫佇列的iterator時候執行(1)(4)(7)後佇列狀態圖: image.png 呼叫hasNext()時候知道nextNode != null所以返回true. 然後呼叫next()方法執行(2)(5)(7)後,返回zlx,佇列狀態圖 image.png 也就說第一次呼叫佇列的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()後情況為: image.png 假如執行緒1呼叫next前另外執行緒把佇列裡面的zlx刪除了,現在佇列狀態: image.png 現在在呼叫next方法(2)(5)(7)後的狀態為: image.png 所以返回x=zlx; 試驗2的結果很明顯,這裡不再說了,下面看看試驗3 最後還有一個remove方法,他僅僅是把最後一次next時候記錄的節點內容重置為null,並且記錄節點為null,下面圖解說下: 第一次呼叫hasNext後 image.png 然後呼叫remove方法因為lastRet=null所以丟擲了異常,其實應該先呼叫next方法在呼叫remove方法。 image.png 這樣是OK的先呼叫next方法設定lastRet,然後在呼叫remove刪除。 然後看remove裡面並沒有看到有對佇列裡面的頭尾節點進行操作,也就說並沒有在佇列中移除該元素的操作,乍一看這有問題,但是沒問題:下面看刪除zlx後佇列狀態 image.png 也就是remove僅僅把節點內容變為null,所以head還是指向這個元素(注意本節講的都是不帶哨兵節點的佇列,正常情況下佇列一開始有個null的哨兵節點,如果本節考慮的話,那麼上面的圖應該有兩個null節點,一個是哨兵,一個是zlx節點變成的) 而poll時候如果節點內容為null則會繼續檢視後繼節點,所以這裡remove簡單的把節點內容變為null即可。

四、總結

併發佇列裡面的迭代器通過使用nextItem保留建立迭代器時候的節點的值,保證了在呼叫hasNext和next方法之間其他執行緒刪除該元素後還可以正常返回刪除節點的內容,並不丟擲異常,之所以說是弱一致性是因為呼叫next時候該元素已經不在佇列裡面了,但是迭代返回還可以返回。另外remove操作並沒有立刻把刪除的原始從佇列中幹掉,而是在出隊時候從佇列裡面解除,讓它變為自引用節點,等待被垃圾回收。  


加多

加多

高階 Java 攻城獅 at 阿里巴巴加多,目前就職於阿里巴巴,熱衷併發程式設計、ClassLoader,Spring等開源框架,分散式RPC框架dubbo,springcloud等;愛好音樂,運動。微信公眾號:技術原始積累。知識星球賬號:技術原始積累