fail-fast
在網上搜了下fail-fast的解釋,很多人說fail-fast是Java中集合的一種錯誤檢測機制,比如下面這個網友的解釋:
fail-fast 機制是java集合(Collection)中的一種錯誤機制,當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件,
其實fail-fast機制并不是Java集合特有的機制,它是一個通用的系統設計思想,看下維基百科的解釋
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.
從上面的解釋中可以了解到:fail-fast是一種錯誤檢測機制,一旦檢測到可能發生錯誤,就立馬拋出例外,程式不繼續往下執行,
public UserDomain queryUserById(String userId){
if(userId==null||"".equals(userId)){
throw new RuntimeException("error params...");
}
//do something
}
上面的列子就是一個快速失敗的列子,而且是我們開發中經常會用到的錯誤檢測的方式,這樣做能及早發現問題,不讓明顯錯誤的代碼繼續往下運行,而且自己拋出的例外更更容易定位問題,
集合中的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操作的原因,
但是,很多時候,我們是有需求需要過濾集合的,比如洗掉其中一部分元素,那么應該如何做呢?有幾種方法可供參考:
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中元素的下標,可能存在漏刪的情況,
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的值,那么就不會再拋出例外了,其實作代碼如下:
?
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、使用增強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);
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的類等,
參考
- https://www.hollischuang.com/archives/3542
- https://www.hollischuang.com/archives/3304
公眾號推薦
歡迎大家關注我的微信公眾號「程式員自由之路」

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/178296.html
標籤:Java
