今天處理一個多線程業務,這個業務原來是在單線程中運行的,如今對這個業務進行改造,使其能在多線程中運行以加快處理效率。結果拋出了ConcurrentModificationException異常。究其緣由,原來此業務使用了ArrayList來存儲數據。而ArrayList集合是不支持在多線程中使用的,由於在多線程中,一個線程經過iterator去遍歷某集合的過程當中,若該集合內容被其餘線程所改變,那麼(遍歷線程的集合)就會拋出ConcurrentModificationException異常。這是集合的fai-fast機制(具體原理請見後面內容)java
fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操做時,就可能會產生fail-fast事件。例如:當某一個線程A經過iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。爲何說是有可能會產生fail-fast事件?由於迭代器的快速失敗行爲沒法獲得保證,由於通常來講,不可能對是否出現不一樣步併發修改作出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。所以,爲提升這類迭代器的正確性而編寫一個依賴於此異常的程序是錯誤的作法:迭代器的快速失敗行爲應該僅用於檢測 bug。數組
下面經過一個示例來展現Fail-Fast安全
import java.util.*; import java.util.concurrent.*; /** * java集合中Fast-Fail的測試程序。 * * fast-fail事件產生的條件:當多個線程對Collection進行操做時,若其中某一個線程經過iterator去遍歷集合時,該集合的內容被其餘線程所改變;則會拋出ConcurrentModificationException異常。 * fast-fail解決辦法:經過util.concurrent集合包下的相應類去處理,則不會產生fast-fail事件。 * * 本例中,分別測試ArrayList和CopyOnWriteArrayList這兩種狀況。ArrayList會產生fast-fail事件,而CopyOnWriteArrayList不會產生fast-fail事件。 * 使用ArrayList時,會產生fast-fail事件,拋出ConcurrentModificationException異常;定義以下: * private static List<String> list = new ArrayList<String>(); * 使用時CopyOnWriteArrayList,不會產生fast-fail事件;定義以下: * private static List<String> list = new CopyOnWriteArrayList<String>(); * * @author kucs */ public class FastFailTest { private static List<String> list = new ArrayList<String>(); //private static List<String> list = new CopyOnWriteArrayList<String>(); public static void main(String[] args) { // 同時啓動兩個線程對list進行操做! new ThreadOne().start(); new ThreadTwo().start(); } private static void printAll() { System.out.println(""); String value = null; Iterator iter = list.iterator(); while(iter.hasNext()) { value = (String)iter.next(); System.out.print(value+", "); } } /** * 向list中依次添加0,1,2,3,4,5,每添加一個數以後,就經過printAll()遍歷整個list */ private static class ThreadOne extends Thread { public void run() { int i = 0; while (i<6) { list.add(String.valueOf(i)); printAll(); i++; } } } /** * 向list中依次添加10,11,12,13,14,15,每添加一個數以後,就經過printAll()遍歷整個list */ private static class ThreadTwo extends Thread { public void run() { int i = 10; while (i<16) { list.add(String.valueOf(i)); printAll(); i++; } } } }
運行結果:數據結構
0, 10, 0, 10, 1, 0, 10, 1, 2, 0, 0, 10, 1, 2, 3, 0, 10, 1, 2, 3, 4, 0, 10, 1, 2, 3, 4, 5, Exception in thread "Thread-1" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at FastFailTest.printAll(FastFailTest.java:35) at FastFailTest.access$300(FastFailTest.java:18) at FastFailTest$ThreadTwo.run(FastFailTest.java:62)
(如下內容參照博客:http://blog.csdn.net/chenssy/article/details/38151189)多線程
經過上面的示例和講解,我初步知道fail-fast產生的緣由就在於程序在對 collection 進行迭代時,某個線程對該 collection 在結構上對其作了修改,這時迭代器就會拋出 ConcurrentModificationException 異常信息,從而產生 fail-fast。併發
要了解fail-fast機制,咱們首先要對ConcurrentModificationException 異常有所瞭解。當方法檢測到對象的併發修改,但不容許這種修改時就拋出該異常。同時須要注意的是,該異常不會始終指出對象已經由不一樣線程併發修改,若是單線程違反了規則,一樣也有可能會拋出改異常。源碼分析
誠然,迭代器的快速失敗行爲沒法獲得保證,它不能保證必定會出現該錯誤,可是快速失敗操做會盡最大努力拋出ConcurrentModificationException異常,因此所以,爲提升此類操做的正確性而編寫一個依賴於此異常的程序是錯誤的作法,正確作法是:ConcurrentModificationException 應該僅用於檢測 bug。下面我將以ArrayList爲例進一步分析fail-fast產生的緣由。測試
從前面咱們知道fail-fast是在操做迭代器時產生的。如今咱們來看看ArrayList中迭代器的源代碼:this
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 ,他們的值在何時發生改變的。spa
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的機制,下面我就產生的緣由提出解決方案。這裏有兩種解決方案:
方案一:在遍歷過程當中全部涉及到改變modCount值得地方所有加上synchronized或者直接使用Collections.synchronizedList,這樣就能夠解決。可是不推薦,由於增刪形成的同步鎖可能會阻塞遍歷操做。
方案二:使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。
CopyOnWriteArrayList爲什麼物?ArrayList 的一個線程安全的變體,其中全部可變操做(add、set 等等)都是經過對底層數組進行一次新的複製來實現的。 該類產生的開銷比較大,可是在兩種狀況下,它很是適合使用。1:在不能或不想進行同步遍歷,但又須要從併發線程中排除衝突時。2:當遍歷操做的數量大大超過可變操做的數量時。遇到這兩種狀況使用CopyOnWriteArrayList來替代ArrayList再適合不過了。那麼爲何CopyOnWriterArrayList能夠替代ArrayList呢?
第1、CopyOnWriterArrayList的不管是從數據結構、定義都和ArrayList同樣。它和ArrayList同樣,一樣是實現List接口,底層使用數組實現。在方法上也包含add、remove、clear、iterator等方法。
第2、CopyOnWriterArrayList根本就不會產生ConcurrentModificationException異常,也就是它使用迭代器徹底不會產生fail-fast機制。請看:
private static class COWIterator<E> implements ListIterator<E> { /** 省略此處代碼 */ public E next() { if (!(hasNext())) throw new NoSuchElementException(); return this.snapshot[(this.cursor++)]; } /** 省略此處代碼 */ }
CopyOnWriterArrayList的方法根本就沒有像ArrayList中使用checkForComodification方法來判斷expectedModCount 與 modCount 是否相等。它爲何會這麼作,憑什麼能夠這麼作呢?咱們以add方法爲例:
public boolean add(E paramE) { ReentrantLock localReentrantLock = this.lock; localReentrantLock.lock(); try { Object[] arrayOfObject1 = getArray(); int i = arrayOfObject1.length; Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1); arrayOfObject2[i] = paramE; setArray(arrayOfObject2); int j = 1; return j; } finally { localReentrantLock.unlock(); } } final void setArray(Object[] paramArrayOfObject) { this.array = paramArrayOfObject; }
CopyOnWriterArrayList的add方法與ArrayList的add方法有一個最大的不一樣點就在於,下面三句代碼:
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1); arrayOfObject2[i] = paramE; setArray(arrayOfObject2);
就是這三句代碼使得CopyOnWriterArrayList不會拋ConcurrentModificationException異常。他們所展示的魅力就在於copy原來的array,再在copy數組上進行add操做,這樣作就徹底不會影響COWIterator中的array了。
因此CopyOnWriterArrayList所表明的核心概念就是:任何對array在結構上有所改變的操做(add、remove、clear等),CopyOnWriterArrayList都會copy現有的數據,再在copy的數據上修改,這樣就不會影響COWIterator中的數據了,修改完成以後改變原有數據的引用便可。同時這樣形成的代價就是產生大量的對象,同時數組的copy也是至關有損耗的。