1. 程式人生 > >Java集合框架中的快速失敗(fail—fast)機制詳解

Java集合框架中的快速失敗(fail—fast)機制詳解

block rac sms 編程 參數 failsafe next ID ted

先說結論:在用for遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除),則會拋出ConcurrentModificationException。在單線程下用叠代器遍歷修改,則不會報錯。在多線程環境下則會報錯。

??原理:叠代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當叠代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否為expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。
??這裏異常的拋出條件是檢測到 modCount!=expectedmodCount

這個條件。如果集合發生變化時修改modCount值剛好又設置為了expectedmodCount值,則異常不會拋出。因此,不能依賴於這個異常是否拋出而進行並發操作的編程,這個異常只建議用於檢測並發修改的bug。

我們來檢測單線程下用增強for循環來更新數據會不會報錯:

import java.util.*;
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for(int i=0;i<10;i++) {
        list.add(i);
    }
    for
(int n:list){ if(n==6){ list.set(n, 16); } } System.out.println(list.toString()); }

輸出是[0, 1, 2, 3, 4, 5, 16, 7, 8, 9]

那如果在遍歷中刪除參數呢?

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for(int i=0;i<10;i++) {
        list.add
(i); } for(int n:list){ if(n==6){ list.remove(n); } } System.out.println(list.toString()); }
->Exception in thread "main" java.util.ConcurrentModificationException

這是為什麽呢?讓我們來看看set的源碼:

public E set(int index, E element) {
   rangeCheck(index);

   E oldValue = elementData(index);
   elementData[index] = element;
   return oldValue;
}

我們發現,進行set操作的時候,modCount並沒有自增,所以不會報錯。

那如果用叠代器進行遍歷remove呢?

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for(int i=0;i<10;i++) {
        list.add(i);
    }
    Iterator<Integer> it = list.iterator();
    while (it.hasNext()) {
        if (it.next() == 6) {
            it.remove();
        }
    }
    System.out.println(list.toString());
}
->[0, 1, 2, 3, 4, 5, 7, 8, 9]

這是因為:

  • 叠代器是作為當前集合的內部類實現的,當叠代器創建的時候保持了當前集合的引用;
  • 集合內部維護一個int變量modCount,用來記錄集合被修改的次數,比如add,remove等都會使該字段遞增;
  • modCount這個參數記錄了某個List改變大小的次數,如果modCount改變的不符合預期,那麽就會拋出異常。
  • 叠代器內部也維護著當前集合的修改次數的字段,叠代器創建時該字段初始化為集合的modCount值
  • 當每一次叠代時,叠代器會比較叠代器維護的字段和modCount的值是否相等,如果不相等就拋ConcurrentModifiedException異常;
  • 當然,如果用叠代器調用remove方法,那麽集合和叠代器維護的修改次數都會遞增,以保持兩個狀態的一致。

如下面ArrayList繼承的AbstractList源碼所示:

  • 定義了modCount
protected transient int modCount = 0;

ArrayList源碼:

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();
    }
}
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    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

    return oldValue;
}
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

可以發現進行remove操作時變量modCount自增了

??Ps:java.util包下的集合類都是快速失敗的,不能在多線程下發生並發修改(叠代過程中被修改)。

安全失敗(fail—safe)

??采用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先復制原有集合內容,在拷貝的集合上進行遍歷。

??原理:由於叠代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被叠代器檢測到,所以不會觸發Concurrent Modification Exception。
??缺點:基於拷貝內容的優點是避免了Concurrent Modification Exception,但同樣地,叠代器並不能訪問到修改後的內容,即:叠代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改叠代器是不知道的。
??Ps:java.util.concurrent包下的容器都是安全失敗,可以在多線程下並發使用,並發修改。

Java集合框架中的快速失敗(fail—fast)機制詳解