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
的機制。比如,像最終一致性的ConcurrentHashMap
、CopyOnWriterArrayList
等都是沒有fast-fail
的。
那麼,fail-fast
是怎麼實現的呢?
細心的同學可能會發現,像ArrayList
、HashMap
中都有一個屬性叫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();
}
如上,在該方法中對modCount
和expectedModCount
進行了比較,如果二者不想等,則丟擲CMException
。
那麼,modCount
和expectedModCount
是什麼?是什麼原因導致他們的值不想等的呢?
modCount
是ArrayList
中的一個成員變數。它表示該集合實際被修改的次數。
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
當使用以上程式碼初始化集合之後該變數就有了。初始值為0
。
expectedModCount
是ArrayList
中的一個內部類——Itr
中的成員變數。
Iterator iterator = userNames.iterator();
以上程式碼,即可得到一個 Itr類,該類實現了Iterator
介面。
expectedModCount
表示這個迭代器預期該集合被修改的次數。其值隨著Itr
被建立而初始化。只有通過迭代器對集合進行操作,該值才會改變。
那麼,接著我們看下userNames.remove(userName);
方法裡面做了什麼事情,為什麼會導致expectedModCount
和modCount
的值不一樣。
通過翻閱程式碼,我們也可以發現,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
做任何操作。
所以導致產生異常的原因是:remove
和add
操作會導致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
進行元素刪除、使用Stream
的filter
、使用fail-safe
的類等。