Java集合框架中的快速失敗(fail—fast)機制詳解
先說結論:在用for遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除),則會拋出ConcurrentModificationException。在單線程下用叠代器遍歷修改,則不會報錯。在多線程環境下則會報錯。
??原理:叠代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當叠代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否為expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。
??這裏異常的拋出條件是檢測到 modCount!=expectedmodCount
我們來檢測單線程下用增強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)機制詳解