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

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

  1. 併發修改「一般」致使及時失敗
  2. 單線程修改也可能致使及時失敗的「誤報」
  3. 迭代器會「丟失」某些併發修改行爲,讓及時失敗失效

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

及時失敗

及時失敗也叫快速失敗,fast-fail。
「及時失敗」的迭代器並非一種完備的處理機制,而只是「善意地」捕獲併發錯誤,所以只能做爲併發問題的預警指示器。它們採用的實現方式是,將計數器的變化與容器關聯起來:若是在迭代期間計數器被修改,那麼hasNext或next將拋出ConcurrentModificationException。然而,這種檢查是在沒有同步的狀況下進行的,所以可能會看到失效的計數器,而迭代器可能並無意識到已經發生了修改。這是一種設計上的權衡,從而下降併發修改操做的檢測代碼對程序性能帶來的影響。github

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

幾種非線程安全的表明狀況

併發修改「一般」致使及時失敗

「一般」是由於及時失敗的「善意」性質,它不少時候會給咱們提醒,但有時候也不會給出提醒,有時候甚至給出某種意義上的錯誤提醒。這一小節針對正常的狀況,這是咱們考察一個機制是否值得「採納並完善」的根本屬性。多線程

構造下列程序:併發

…
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);
        }
    }
}
…

忽略細節,假設有多個線程在同時執行run方法,操做users集合。這時,「一般」會致使及時失敗。這裏的異常可能從next或remove方法中拋出(固然這裏是從next,由於next先執行):源碼分析

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方法正常執行(不拋出異常)才能夠修改這個值,與modCount保持同步。this

所以,若是在線程A正常迭代的過程當中,線程B修改了users集合,modCount就會發生變化,這時,線程B的expectedModCount可以與modCount保持同步,線程A的expectedModCount卻發現本身與modCount再也不同步,從而拋出ConcurrentModificationException異常。線程

扯遠些:
對於線程安全的集合類而言,咱們不但願任何失敗。但對於非線程安全的類,有人認爲「應該在假設線程安全的狀況下使用」,因此及時失敗機制徹底沒有必要;有人認爲「集合類的狀態太多(全部非線程安全域的狀態數量的乘積),併發使用時應該給出錯誤提醒,不然很難排查併發問題」,因此及時失敗機制頗有必要。這個問題見仁見智,我的支持後者觀點。

因此,這種及時失敗的檢查是不完備的。

單線程修改也可能致使及時失敗的「誤報」

多線程併發修改集合時,拋出ConcurrentModificationException異常做爲及時失敗的提醒,每每是咱們指望的結果。然而,若是在單線程遍歷迭代器的過程當中修改了集合,也會拋出ConcurrentModificationException異常,看起來發生了及時失敗。這不是咱們指望的結果,是一種及時失敗的誤報。

咱們改用集合的remove方法移除user「張三」:

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

假設只有一個線程執行run方法,在」張三」被刪除以後,下一次執行next方法時,仍舊會拋出ConcurrentModificationException異常,也就是致使了及時失敗。

這時由於集合的remove方法並無維護集合修改的狀態(如對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;
    }
…
}

這也讓咱們更容易理解及時失敗的本質——依託於對集合修改狀態的維護。這裏的主要緣由看起來是「集合的remove方法破壞了正常維護的集合修改狀態」,但對於使用者而言,集合在單線程環境下卻拋出了ConcurrentModificationException異常,這是因爲及時失敗機制沒有區分單線程與多線程的狀況,統一給出一樣的提醒(拋出ConcurrentModificationException異常),於是是及時失敗的誤報。

迭代器會「丟失」某些併發修改行爲,讓及時失敗失效

除了誤報,及時失敗之僅限於「善意」(有提醒就是「善意」的,沒有也不是「惡意」的)還體如今其可能「丟失」某些併發修改行爲。在這裏,「丟失」意味着不提醒——某些線程併發修改了當前集合,但沒有拋出ConcurrentModificationException異常,及時失敗機制失效了。

主動避過及時失敗的檢查

利用hasNext方法提早結束線程,能夠主動避過及時失敗的檢查,從而致使修改行爲的丟失:

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

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

推廣到多線程的情景是同樣的,由於size是共享的。

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

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

假設線程A進入迭代器的remove方法,線程B進入迭代器的next方法,如今線程A執行集合的remove方法:

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

首先,假設沒有其餘線程併發修改,則兩個線程均可以經過checkForComodification()的檢查;而後線程A快速的執行集合的remove方法;待線程A執行完集合的remove方法,因爲線程B以前已經經過了檢查,如今就沒法意識到「users集合在線程A中已經發生了變化」。另外,由於幾乎徹底不存在同步措施,modCount的修改也存在競態條件,其餘狀態也沒法保證是否有效。

總結

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

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

參考:

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

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

相關文章
相關標籤/搜索