在JDK的Collection中我們時常會看到類似這樣的話:
例如,ArrayList:
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽 最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭 代器的快速失败行为应该仅用于检测 bug。
HashMap中:
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大 努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应 该仅用于检测程序错误。
在這兩段話中反覆地提到」快速失敗」。那麼何為」快速失敗」機制呢?
「快速失敗」也就是fail-fast,它是Java集合的一種錯誤偵測機制。當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。記住是有可能,而不是一定。例如:假設存在兩個執行緒(執行緒1、執行緒2),執行緒1透過Iterator在遍歷集合A中的元素,在某個時候執行緒2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程式就會拋出ConcurrentModificationException 異常,進而產生fail-fast機制。
一、fail-fast範例
public class FailFastTest { private static List<Integer> list = new ArrayList<>(); /** * @desc:线程one迭代list * @Project:test * @file:FailFastTest.java * @Authro:chenssy * @data:2014年7月26日 */ private static class threadOne extends Thread{ public void run() { Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ int i = iterator.next(); System.out.println("ThreadOne 遍历:" + i); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * @desc:当i == 3时,修改list * @Project:test * @file:FailFastTest.java * @Authro:chenssy * @data:2014年7月26日 */ private static class threadTwo extends Thread{ public void run(){ int i = 0 ; while(i < 6){ System.out.println("ThreadTwo run:" + i); if(i == 3){ list.remove(i); } i++; } } } public static void main(String[] args) { for(int i = 0 ; i < 10;i++){ list.add(i); } new threadOne().start(); new threadTwo().start(); } }
運行結果:
ThreadOne 遍历:0 ThreadTwo run:0 ThreadTwo run:1 ThreadTwo run:2 ThreadTwo run:3 ThreadTwo run:4 ThreadTwo run:5 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at test.ArrayListTest$threadOne.run(ArrayListTest.java:23
二、fail-fast產生原因
#透過上面的範例和解說,我初步知道fail-fast產生的原因就在於程式在對collection 進行迭代時,某個執行緒對該collection 在結構上對其做了修改,這時迭代器就會拋出ConcurrentModificationException 例外訊息,從而產生fail-fast。
要了解fail-fast機制,我們首先要對ConcurrentModificationException 例外有所了解。當方法偵測到物件的並發修改,但不允許這種修改時就拋出該異常。同時要注意的是,該異常不會始終指出物件已經由不同執行緒並發修改,如果單執行緒違反了規則,同樣也有可能會拋出變更異常。
誠然,迭代器的快速失敗行為無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出ConcurrentModificationException異常,所以因此,為提高此類操作的正確性而編寫一個依賴此異常的程式是錯誤的做法,正確做法是:ConcurrentModificationException 應該僅用於檢測bug。下面我將以ArrayList為例進一步分析fail-fast產生的原因。
從前面我們知道fail-fast是在操作迭代器時產生的。現在我們來看看ArrayList中迭代器的原始碼:
private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = ArrayList.this.modCount; public boolean hasNext() { return (this.cursor != ArrayList.this.size); } public E next() { checkForComodification(); /** 省略此处代码 */ } public void remove() { if (this.lastRet < 0) throw new IllegalStateException(); checkForComodification(); /** 省略此处代码 */ } final void checkForComodification() { if (ArrayList.this.modCount == this.expectedModCount) return; throw new ConcurrentModificationException(); } }
從上面的原始程式碼我們可以看出,迭代器在呼叫next()、remove()方法時都是呼叫checkForComodification()方法,此方法主要是偵測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,進而產生fail-fast機制。所以要弄清楚為什麼會產生fail-fast機制我們就必須要弄清楚為什麼modCount != expectedModCount ,他們的值在什麼時候改變的。
expectedModCount 是在Itr中定義的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能會修改的,所以會變的就是modCount。 modCount是在 AbstractList 中定義的,為全域變數:
protected transient int modCount = 0;
那麼他什麼時候因為什麼原因而改變呢?請看ArrayList的原始碼:
public boolean add(E paramE) { ensureCapacityInternal(this.size + 1); /** 省略此处代码 */ } private void ensureCapacityInternal(int paramInt) { if (this.elementData == EMPTY_ELEMENTDATA) paramInt = Math.max(10, paramInt); ensureExplicitCapacity(paramInt); } private void ensureExplicitCapacity(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */ } public boolean remove(Object paramObject) { int i; if (paramObject == null) for (i = 0; i < this.size; ++i) { if (this.elementData[i] != null) continue; fastRemove(i); return true; } else for (i = 0; i < this.size; ++i) { if (!(paramObject.equals(this.elementData[i]))) continue; fastRemove(i); return true; } return false; } private void fastRemove(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */ } public void clear() { this.modCount += 1; //修改modCount /** 省略此处代码 */ }
從上面的原始碼我們可以看出,ArrayList中無論add、remove、clear方法只要是涉及了改變ArrayList元素的數量的方法都會導致modCount的改變。所以我們這裡可以初步判斷由於expectedModCount 得值與modCount的改變不同步,導致兩者之間不等而產生fail-fast機制。知道產生fail-fast產生的根本原因了,我們可以有以下場景:
有兩個線程(線程A,線程B),其中線程A負責遍歷list、線程B修改list。執行緒A在遍歷list過程的某個時候(此時expectedModCount = modCount=N),執行緒啟動,同時執行緒B增加一個元素,這是modCount的值改變(modCount 1 = N 1)。當執行緒A繼續遍歷執行next方法時,通告checkForComodification方法發現expectedModCount = N ,而modCount = N 1,兩者不等,這時就拋出ConcurrentModificationException 異常,從而產生fail-fast機制。
所以,直到這裡我們已經完全了解了fail-fast產生的根本原因了。知道了原因就好找解決方法了。
三、fail-fast解決方法
透過前面的實例、原始碼分析,我想各位已經基本了解了fail-fast的機制,下面我就產生的原因提出解決方案。這裡有兩種解決方案:
方案一: 在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或直接使用Collections.synchronizedList,這樣就可以解決。但不建議,因為增刪造成的同步鎖定可能會阻塞遍歷操作。
方案二: 使用CopyOnWriteArrayList來取代ArrayList。推薦使用該方案。
以上是fail-fast機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!