從源碼分析非線程安全集合類的不安全迭代器

非線程安全集合類(這里的集合指容器Collection,非Set)的迭代器結合了及時失敗機制,但仍然是不安全的。這種不安全表現在許多方面:

  1. 并發(fā)修改“通常”導致及時失敗
  2. 單線程修改也可能導致及時失敗的“誤報”
  3. 迭代器會“丟失”某些并發(fā)修改行為,讓及時失敗失效

如果不了解其不安全之處就隨意使用,就像給程序埋下了地雷,隨時可能引爆,卻不可預知。
ArrayList是一個常用的非線程安全集合,下面以基于ArrayList講解幾種代表情況。

及時失敗

及時失敗也叫快速失敗,fast-fail。
“及時失敗”的迭代器并不是一種完備的處理機制,而只是“善意地”捕獲并發(fā)錯誤,因此只能作為并發(fā)問題的預警指示器。它們采用的實現方式是,將計數器的變化與容器關聯起來:如果在迭代期間計數器被修改,那么hasNext或next將拋出ConcurrentModificationException。然而,這種檢查是在沒有同步的情況下進行的,因此可能會看到失效的計數器,而迭代器可能并沒有意識到已經發(fā)生了修改。這是一種設計上的權衡,從而降低并發(fā)修改操作的檢測代碼對程序性能帶來的影響。

然而,及時失敗機制十分簡潔(簡單&清晰),同時對集合的性能影響十分小,所以大部分非線程安全的集合類仍然使用這種機制來進行“善意”的提醒。

幾種非線程安全的代表情況

并發(fā)修改“通常”導致及時失敗

“通常”是因為及時失敗的“善意”性質,它很多時候會給我們提醒,但有時候也不會給出提醒,有時候甚至給出某種意義上的錯誤提醒。這一小節(jié)針對正常的情況,這是我們考察一個機制是否值得“采納并完善”的根本屬性。

構造下列程序:

…
private Collection users = new ArrayList(); // 所以應使用CopyOnWriteArrayList
…
users.add(new User("張三",28));
users.add(new User("李四",25));
users.add(new User("王五",31));
…
public void run() {
    Iterator itrUsers = users.iterator();
    while(itrUsers.hasNext()){
        System.out.println("aaaa");
        User user = (User)itrUsers.next();
        if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
            itrUsers.remove();
        } else { // 正常輸出
            System.out.println(user);
        }
    }
}
…

忽略細節(jié),假設有多個線程在同時執(zhí)行run方法,操作users集合。這時,“通?!睍е录皶r失敗。這里的異??赡軓膎ext或remove方法中拋出(當然這里是從next,因為next先執(zhí)行):

private class Itr implements Iterator<E> {
…
    public boolean hasNext() {
        return cursor != size;
    }
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    public void remove() { // 迭代器的remove方法
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet); // 集合的remove方法
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
…
}

實際檢查并拋出異常的是checkForComodification方法:

private class Itr implements Iterator<E> {
…
int expectedModCount = modCount;
    …
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    …
}

modCount是當前集合的版本號,每次修改(增刪改)集合都會加 1;expectedModCount是當前迭代器的版本號,在迭代器實例化時初始化為modCount,只有remove方法正常執(zhí)行(不拋出異常)才可以修改這個值,與modCount保持同步。

因此,如果在線程A正常迭代的過程中,線程B修改了users集合,modCount就會發(fā)生變化,這時,線程B的expectedModCount能夠與modCount保持同步,線程A的expectedModCount卻發(fā)現自己與modCount不再同步,從而拋出ConcurrentModificationException異常。

扯遠些:
對于線程安全的集合類而言,我們不希望任何失敗。但對于非線程安全的類,有人認為“應該在假設線程安全的情況下使用”,所以及時失敗機制完全沒有必要;有人認為“集合類的狀態(tài)太多(所有非線程安全域的狀態(tài)數量的乘積),并發(fā)使用時應該給出錯誤提醒,否則很難排查并發(fā)問題”,所以及時失敗機制很有必要。這個問題見仁見智,個人支持后者觀點。

所以,這種及時失敗的檢查是不完備的。

單線程修改也可能導致及時失敗的“誤報”

多線程并發(fā)修改集合時,拋出ConcurrentModificationException異常作為及時失敗的提醒,往往是我們期望的結果。然而,如果在單線程遍歷迭代器的過程中修改了集合,也會拋出ConcurrentModificationException異常,看起來發(fā)生了及時失敗。這不是我們期望的結果,是一種及時失敗的誤報。

我們改用集合的remove方法移除user“張三”:

…
public void run() {
    …
        if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
            users.remove(user); // itrUsers.remove();
        } else { // 正常輸出
            System.out.println(user);
        }
    …
}
…

假設只有一個線程執(zhí)行run方法,在”張三”被刪除之后,下一次執(zhí)行next方法時,仍舊會拋出ConcurrentModificationException異常,也就是導致了及時失敗。

這時因為集合的remove方法并沒有維護集合修改的狀態(tài)(如對modCount&expectedModCount組合的修改和檢查):

public class ArrayList<E> extends AbstractList<E>
…
    public boolean remove(Object o) { // 集合的remove方法
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
…
}

這也讓我們更容易理解及時失敗的本質——依托于對集合修改狀態(tài)的維護。這里的主要原因看起來是“集合的remove方法破壞了正常維護的集合修改狀態(tài)”,但對于使用者而言,集合在單線程環(huán)境下卻拋出了ConcurrentModificationException異常,這是由于及時失敗機制沒有區(qū)分單線程與多線程的情況,統(tǒng)一給出同樣的提醒(拋出ConcurrentModificationException異常),因而是及時失敗的誤報。

迭代器會“丟失”某些并發(fā)修改行為,讓及時失敗失效

除了誤報,及時失敗之僅限于“善意”(有提醒就是“善意”的,沒有也不是“惡意”的)還體現在其可能“丟失”某些并發(fā)修改行為。在這里,“丟失”意味著不提醒——某些線程并發(fā)修改了當前集合,但沒有拋出ConcurrentModificationException異常,及時失敗機制失效了。

主動避過及時失敗的檢查

利用hasNext方法提前結束線程,可以主動避過及時失敗的檢查,從而導致修改行為的丟失:

private class Itr implements Iterator<E> {
…
    public boolean hasNext() {
        return cursor != size; // 思考:如果刪除了集合的倒數第二個元素,會發(fā)生什么?
    }
…
}

還是單線程的場景下,假設我們刪除了集合的倒數第二個元素。這時next方法導致cursor=oldSize-1,同時remove方法導致newSize=oldSize-1(oldSize是集合修改之前的size值,newSize集合修改之后的);所以hasNext方法會返回false,讓用戶誤以為集合迭代已經結束(實際上還有最后一個元素),從而循環(huán)終止(在我們的程序里用hasNext判斷是否結束),無法拋出ConcurrentModificationException異常,及時失敗失效了。

推廣到多線程的情景是一樣的,因為size是共享的。

及時失敗的實現是非線程安全的

很容易忽略的一點是,上述集合修改狀態(tài)的維護本身就是在沒有同步的情況下進行的,因此可能看到更多(遠比上述要多)失效的集合修改狀態(tài),使迭代器意識不到集合發(fā)生了修改,這是一種競態(tài)條件(Race Condition)。

假設線程A進入迭代器的remove方法,線程B進入迭代器的next方法,現在線程A執(zhí)行集合的remove方法:

private class Itr implements Iterator<E> {
…
    public void remove() {
        …
            ArrayList.this.remove(lastRet);
        …
    }
…
}

首先,假設沒有其他線程并發(fā)修改,則兩個線程都可以通過checkForComodification()的檢查;然后線程A快速的執(zhí)行集合的remove方法;待線程A執(zhí)行完集合的remove方法,由于線程B之前已經通過了檢查,現在就無法意識到“users集合在線程A中已經發(fā)生了變化”。另外,因為幾乎完全不存在同步措施,modCount的修改也存在競態(tài)條件,其他狀態(tài)也無法保證是否有效。

總結

上面看到了非線程安全集合類的迭代器是不安全的,但在單線程的環(huán)境下,這些集合類在性能、維護難度等方面仍然具有不可替代的優(yōu)勢。那么該如何在兼具一定程度線程安全的前提下,更好的發(fā)揮內建集合類的優(yōu)勢呢?總結起來無非兩點:

  1. 使用非線程安全的集合時(實際上對于某些“線程安全”的集合類,其迭代器也是線程不安全的),迭代過程中需要用戶自覺維護,不修改該集合。
  2. 應盡可能明確線程安全的需求等級,做好一致性、活躍性、性能等方面的平衡,再針對性的使用相應的集合類。

參考:

  • 傳智播客_張孝祥_Java多線程與并發(fā)庫高級應用視頻教程/19_傳智播客_張孝祥_java5同步集合類的應用.avi

本文鏈接:源碼|從源碼分析非線程安全集合類的不安全迭代器
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協議發(fā)布,歡迎轉載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,770評論 2 17
  • 1.Java集合框架是什么?說出一些集合框架的優(yōu)點? 每種編程語言中都有集合,最初的Java版本包含幾種集合類:V...
    獨念白閱讀 882評論 0 2
  • 1.Java集合框架是什么?說出一些集合框架的優(yōu)點? 每種編程語言中都有集合,最初的Java版本包含幾種集合類:V...
    joshul閱讀 411評論 0 2
  • 1.Java集合框架是什么?說出一些集合框架的優(yōu)點?每種編程語言中都有集合,最初的Java版本包含幾種集合類:Ve...
    yjaal閱讀 1,245評論 1 10
  • 標簽(空格分隔): Java集合框架 問題思考 什么是集合框架? 為什么用集合框架? 怎么用集合框架? 問題解決 ...
    outSiderYN閱讀 759評論 0 13

友情鏈接更多精彩內容