1. 程式人生 > 其它 >Java容器之錯誤機制fail-fast

Java容器之錯誤機制fail-fast

一、什麼是fail-fast

fail-fast機制是java集合中的一種錯誤機制。首先我們看下維基百科中關於fail-fast的解釋:

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

大概意思是:在系統設計中,快速失效系統一種可以立即報告任何可能表明故障的情況的系統。快速失效系統通常設計用於停止正常操作,而不是試圖繼續可能存在缺陷的過程。這種設計通常會在操作中的多個點檢查系統的狀態,因此可以及早檢測到任何故障。快速失敗模組的職責是檢測錯誤,然後讓系統的下一個最高級別處理錯誤。

其實,這是一種理念,說白了就是在做系統設計的時候先考慮異常情況,一旦發生異常,直接停止並上報。

當使用迭代器迭代時,如果發現集合有修改,則快速失敗做出響應,丟擲ConcurrentModificationException異常。

這種修改有可能是其它執行緒的修改,也有可能是當前執行緒自己的修改導致的,比如迭代的過程中直接呼叫remove()

刪除元素等。

另外,並不是java中所有的集合都有fail-fast的機制。比如,像最終一致性的ConcurrentHashMapCopyOnWriterArrayList等都是沒有fast-fail的。

那麼,fail-fast是怎麼實現的呢?

細心的同學可能會發現,像ArrayListHashMap中都有一個屬性叫modCount,每次對集合的修改這個值都會加1,在遍歷前記錄這個值到expectedModCount中,遍歷中檢查兩者是否一致,如果出現不一致就說明有修改,則丟擲ConcurrentModificationException異常。

二、集合中的fail-fast

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);

以上程式碼,使用增強for迴圈遍歷元素,並嘗試刪除其中的Hollis字串元素。執行以上程式碼,會丟擲以下異常:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.hollis.ForEach.main(ForEach.java:22)

同樣的,可以嘗試下在增強for迴圈中使用add方法新增元素,結果也會同樣丟擲該異常。

三、異常產生的原因

增強for迴圈其實是Java提供的一個語法糖,我們將程式碼反編譯後可以看到增強for迴圈其實是用的是Iterator迭代器。

public static void main(String[] args) {
    // 使用ImmutableList初始化一個List
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}

通過以上程式碼的異常堆疊,我們可以跟蹤到真正丟擲異常的程式碼是:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

該方法是在iterator.next()方法中呼叫的。我們看下該方法的實現:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

如上,在該方法中對modCountexpectedModCount進行了比較,如果二者不想等,則丟擲CMException

那麼,modCountexpectedModCount是什麼?是什麼原因導致他們的值不想等的呢?

modCountArrayList中的一個成員變數。它表示該集合實際被修改的次數。

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

當使用以上程式碼初始化集合之後該變數就有了。初始值為0

expectedModCountArrayList中的一個內部類——Itr中的成員變數。

Iterator iterator = userNames.iterator();

以上程式碼,即可得到一個 Itr類,該類實現了Iterator介面。

expectedModCount表示這個迭代器預期該集合被修改的次數。其值隨著Itr被建立而初始化。只有通過迭代器對集合進行操作,該值才會改變。

那麼,接著我們看下userNames.remove(userName);方法裡面做了什麼事情,為什麼會導致expectedModCountmodCount的值不一樣。

通過翻閱程式碼,我們也可以發現,remove方法核心邏輯如下:

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

可以看到,它只修改了modCount,並沒有對expectedModCount做任何操作。

所以導致產生異常的原因是:removeadd操作會導致modCount和迭代器中的expectedModCount不一致。

四、正確姿勢

至此,我們介紹清楚了不能在foreach迴圈體中直接對集合進行add/remove操作的原因。

但是,很多時候,我們是有需求需要過濾集合的,比如刪除其中一部分元素,那麼應該如何做呢?有幾種方法可供參考:

4.1 直接使用普通for迴圈進行操作

我們說不能在foreach中進行,但是使用普通的for迴圈還是可以的,因為普通for迴圈並沒有用到Iterator的遍歷,所以壓根就沒有進行fail-fast的檢驗。

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (int i = 0; i < 1; i++) {
    if (userNames.get(i).equals("Hollis")) {
        userNames.remove(i);
    }
}
System.out.println(userNames);

這種方案其實存在一個問題,那就是remove操作會改變List中元素的下標,可能存在漏刪的情況。

4.2 直接使用Iterator進行操作

除了直接使用普通for迴圈以外,我們還可以直接使用Iterator提供的remove方法。

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

Iterator iterator = userNames.iterator();

while (iterator.hasNext()) {
    if (iterator.next().equals("Hollis")) {
        iterator.remove();
    }
}
System.out.println(userNames);

如果直接使用Iterator提供的remove方法,那麼就可以修改到expectedModCount的值。那麼就不會再丟擲異常了。其實現程式碼如下:

public void remove(){ 
    if(lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;		
    } catch(IndexOutOfBoundsException ex){
        throw new ConcurrentModificationException();	
    }	 
}

4.3 使用Java 8中提供的filter過濾

Java 8中可以把集合轉換成流,對於流有一種filter操作, 可以對原始Stream進行某項測試,通過測試的元素被留下來生成一個新Stream

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

userNames = userNames.stream().filter(userName ->   !userName.equals("Hollis")).collect(Collectors.toList());
System.out.println(userNames);

4.4 使用增強for迴圈其實也可以

如果,我們非常確定在一個集合中,某個即將刪除的元素只包含一個的話, 比如對Set進行操作,那麼其實也是可以使用增強for迴圈的,只要在刪除之後,立刻結束迴圈體,不要再繼續進行遍歷就可以了,也就是說不讓程式碼執行到下一次的next方法。

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
        break;
    }
}
System.out.println(userNames);

4.5 直接使用fail-safe的集合類

Java中,除了一些普通的集合類以外,還有一些採用了fail-safe機制的集合類。這樣的集合容器在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。

由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發ConcurrentModificationException

ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove();
    }
}

基於拷貝內容的優點是避免了ConcurrentModificationException,但同樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。

java.util.concurrent包下的容器都是安全失敗,可以在多執行緒下併發使用,併發修改。

五、總結

我們使用的增強for迴圈,其實是Java提供的語法糖,其實現原理是藉助Iterator進行元素的遍歷。

但是如果在遍歷過程中,不通過Iterator,而是通過集合類自身的方法對集合進行新增/刪除操作。那麼在Iterator進行下一次的遍歷時,經檢測發現有一次集合的修改操作並未通過自身進行,那麼可能是發生了併發被其他執行緒執行的,這時候就會丟擲異常,來提示使用者可能發生了併發修改,這就是所謂的fail-fast機制。

當然還是有很多種方法可以解決這類問題的。比如使用普通for迴圈、使用Iterator進行元素刪除、使用Streamfilter、使用fail-safe的類等。

https://www.cnblogs.com/54chensongxia/p/12470446.html