fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast事件。例如:當某一個線程A通過iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那么線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
例如:當某一個線程A通過iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那么線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
要了解fail-fast機制,我們首先要對ConcurrentModificationException 異常有所了解。當方法檢測到對象的并發修改,但不允許這種修改時就拋出該異常。同時需要注意的是,該異常不會始終指出對象已經由不同線程并發修改,如果單線程違反了規則,同樣也有可能會拋出改異常。
誠然,迭代器的快速失敗行為無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出ConcurrentModificationException異常,所以因此,為提高此類操作的正確性而編寫一個依賴于此異常的程序是錯誤的做法,正確做法是:ConcurrentModificationException 應該僅用于檢測 bug。
Java中的Iterator非常方便地為所有的數據源提供了一個統一的數據讀取(刪除)的接口,但是新手通常在使用的時候容易報如下錯誤ConcurrentModificationException,原因是在使用迭代器時候底層數據被修改,最常見于數據源不是線程安全的類,如HashMap & ArrayList等。
為什么要有fast-fail
一個案例
來一個新手容易犯錯的例子:
1
2
3
4
5
6
7
8
|
String[] stringArray = { "a" , "b" , "c" , "d" }; List<String> strings = Arrays.asList(stringArray); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { if (iterator.next().equals( "c" )) { strings.remove( "c" ); } } |
更加常見的是在foreach(本質一樣,都是調用Iterator時,操作了原始的strings)語句中:
1
2
3
4
5
|
for (String s : strings) { if (s.equals( "c" )) { strings.remove( "c" ); } } |
產生原因
Java中的集合類(數據源)分為兩種類型:線程安全,位于java.util.concurrent命名目錄下,如CopyOnWriteArrayList;線程不安全:位于java.util目錄下,如ArrayList,HashMap。所謂線程安全是在多線程環境下,這個類還能表現出和行為規范一致的結果,是否文縐縐的...自己google吧。
那既然我們可以有線程安全的集合替代品,那么為什么還要存在ArrayList等呢?因為線程安全的類通常需要通過各種手段去保持對數據訪問的同步,所以通常來說效率會比較差。而如果使用者清楚自身使用場景不存在并發的場景,那么使用非線程安全的集合類在速度上有很大的優勢。
如果開發者在使用時沒有注意,將非線程安全的集合類用在了并發的場景下,比如線程A獲取了ArrayList的iterator,然后線程B通過調用ArrayList.add()修改了ArrayList的數據,此時就有可能會拋出ConcurrentModificationException,注意,這里是有可能。那為啥上面的例子里面也會報這個錯誤呢?上面并不存在并發的情況,摟一眼源碼吧。
Iterator源碼分析
集合類中的fast-fail實現方式都差不多,我們以最簡單的ArrayList為例吧。
ArrayList中會持有一個變量,聲明為:
protected transient int modCount = 0;記錄的是我們對ArrayList修改的次數,比如我們調用 add(),remove()等改變數據的操作時,會將modCount++。
我們通過ArrayList.iterator()返回的是一個實現了Iterator接口的ArrayListIterator:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private class ArrayListIterator implements Iterator<E> { //省略部分代碼....... //初始化時,直接給expectedModCount賦ArrayList的修改次數 private int expectedModCount = modCount; @SuppressWarnings ( "unchecked" ) public E next() { ............ ArrayList<E> ourList = ArrayList. this ; //簡單比較一下當前iterator初始化時ArrayList.modCount的值 //和現在的值是否一致,如果不相等,認為在獲取了當前iterator之后 //有別的位置(有可能是別的線程)修改了ArrayList,直接拋異常 if (ourList.modCount != expectedModCount) { throw new ConcurrentModificationException(); } ............ } } |
原理很簡單,構建Iterator時將當前ArrayList的modCount存起來,以后每一次next()時,判斷ArrayList的modCount值是否有變化,如果有,則是在這個過程中有代碼改變了數據(前面已經提及,只有調用add() remove()等才會去修改modCount的值)。
這也說明了為什么在例子里面我們并不是并發的場景也報錯,因為我們調用ArrayList.remove()時改變了modCount的值。
但是這個東西意義有多大呢?在我看來它有點畫蛇添足的嫌疑。因為在真正的并發場景下,這個fast-fail機制并不能真正即使發現另外線程訪問并修改ArrayList中的數據。原因如下:
再看看modCount的定義protected transient int modCount = 0;。你沒有看錯,它就是一個普通的變量,那么在并發場景下由于共享對象的不可見性,有可能別的線程修改了ArrayList中的modCount,而iterator所在的線程卻并沒有讀取到這個更新。HashMap在1.6以前確實是用了volatile來修飾了modCount來保證各個線程直接對modCount的可見性,但是在1.7里面把這個修飾去掉了,而且認為這是一個bug-->Java7去掉volatitle,可悲啊。。。原因嘛,就是JDK的開發者認為為了這么個破事而需要使用volatitle簡直浪費效率。
就算是使用volatitle就完事大吉了嗎?nono,舉個最簡單的例子,線程A獲取了一個集合類的Iterator,線程B調用了集合類的add(),在add()還沒有執行到modCount++時,線程A獲取執行,并執行結束。在這種場景下,執行結果并不確定。對于ArrayList的Iterator來說,有可能會報一個數組越界的異常...
總結
fast-fail是JDK為了提示開發者將非線程安全的類使用到并發的場景下時,拋出一個異常,及早發現代碼中的問題。但正如本文前面所述,這種機制卻不能絕對正確地給出提示,而且老的JDK版本為了更好地支持這個機制還付出了一定的效率代價。
fast-fail存在的唯一價值可能就是給新手制造一些迷惑,給他深入探索的動力...嘿嘿
補充:
很多網上資料說在使用Iterator時是不能修改數據的,這樣也并不完全準確。即便是支持fast-fail的Iterator本身也提供了remove()來刪除當前遍歷到的元素,例如:ArrayListIterator中的remove(),前面舉的栗子改成如下即可:
1
2
3
4
5
|
while (iterator.hasNext()) { if (iterator.next().equals( "c" )) { iterator.remove( "c" ); } } |