JAVA學習之——fail-fast機制

在JDK的Collection中我們時常會看到類似於這樣的話:

        例如,ArrayList:

注意,迭代器的快速失敗行爲無法得到保證,因爲一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。因此,爲提高這類迭代器的正確性而編寫一個依賴於此異常的程序是錯誤的做法:迭代器的快速失敗行爲應該僅用於檢測 bug。

        HashMap中:

注意,迭代器的快速失敗行爲不能得到保證,一般來說,存在非同步的併發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。

        在這兩段話中反覆地提到”快速失敗”。那麼何爲”快速失敗”機制呢?

        “快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個線程對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。記住是有可能,而不是一定。例如:假設存在兩個線程(線程1、線程2),線程1通過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出 ConcurrentModificationException 異常,從而產生fail-fast機制。

一、fail-fast示例

[java] view plain copy
 print?
  1. public class FailFastTest {  
  2.     private static List<Integer> list = new ArrayList<>();  
  3.       
  4.     /** 
  5.      * @desc:線程one迭代list 
  6.      * @Project:test 
  7.      * @file:FailFastTest.java 
  8.      * @Authro:chenssy 
  9.      * @data:2014年7月26日 
  10.      */  
  11.     private static class threadOne extends Thread{  
  12.         public void run() {  
  13.             Iterator<Integer> iterator = list.iterator();  
  14.             while(iterator.hasNext()){  
  15.                 int i = iterator.next();  
  16.                 System.out.println("ThreadOne 遍歷:" + i);  
  17.                 try {  
  18.                     Thread.sleep(10);  
  19.                 } catch (InterruptedException e) {  
  20.                     e.printStackTrace();  
  21.                 }  
  22.             }  
  23.         }  
  24.     }  
  25.       
  26.     /** 
  27.      * @desc:當i == 3時,修改list 
  28.      * @Project:test 
  29.      * @file:FailFastTest.java 
  30.      * @Authro:chenssy 
  31.      * @data:2014年7月26日 
  32.      */  
  33.     private static class threadTwo extends Thread{  
  34.         public void run(){  
  35.             int i = 0 ;   
  36.             while(i < 6){  
  37.                 System.out.println("ThreadTwo run:" + i);  
  38.                 if(i == 3){  
  39.                     list.remove(i);  
  40.                 }  
  41.                 i++;  
  42.             }  
  43.         }  
  44.     }  
  45.       
  46.     public static void main(String[] args) {  
  47.         for(int i = 0 ; i < 10;i++){  
  48.             list.add(i);  
  49.         }  
  50.         new threadOne().start();  
  51.         new threadTwo().start();  
  52.     }  
  53. }  
 運行結果:

[java] view plain copy
 print?
  1. ThreadOne 遍歷:0  
  2. ThreadTwo run:0  
  3. ThreadTwo run:1  
  4. ThreadTwo run:2  
  5. ThreadTwo run:3  
  6. ThreadTwo run:4  
  7. ThreadTwo run:5  
  8. Exception in thread "Thread-0" java.util.ConcurrentModificationException  
  9.     at java.util.ArrayList$Itr.checkForComodification(Unknown Source)  
  10.     at java.util.ArrayList$Itr.next(Unknown Source)  
  11.     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中迭代器的源代碼:

[java] view plain copy
 print?
  1. private class Itr implements Iterator<E> {  
  2.         int cursor;  
  3.         int lastRet = -1;  
  4.         int expectedModCount = ArrayList.this.modCount;  
  5.   
  6.         public boolean hasNext() {  
  7.             return (this.cursor != ArrayList.this.size);  
  8.         }  
  9.   
  10.         public E next() {  
  11.             checkForComodification();  
  12.             /** 省略此處代碼 */  
  13.         }  
  14.   
  15.         public void remove() {  
  16.             if (this.lastRet < 0)  
  17.                 throw new IllegalStateException();  
  18.             checkForComodification();  
  19.             /** 省略此處代碼 */  
  20.         }  
  21.   
  22.         final void checkForComodification() {  
  23.             if (ArrayList.this.modCount == this.expectedModCount)  
  24.                 return;  
  25.             throw new ConcurrentModificationException();  
  26.         }  
  27.     }  

        從上面的源代碼我們可以看出,迭代器在調用next()、remove()方法時都是調用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,從而產生fail-fast機制。所以要弄清楚爲什麼會產生fail-fast機制我們就必須要用弄明白爲什麼modCount != expectedModCount ,他們的值在什麼時候發生改變的。

        expectedModCount 是在Itr中定義的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能會修改的,所以會變的就是modCount。modCount是在 AbstractList 中定義的,爲全局變量:

[java] view plain copy
 print?
  1. protected transient int modCount = 0;  

那麼他什麼時候因爲什麼原因而發生改變呢?請看ArrayList的源碼:

[java] view plain copy
 print?
  1. public boolean add(E paramE) {  
  2.     ensureCapacityInternal(this.size + 1);  
  3.     /** 省略此處代碼 */  
  4. }  
  5.   
  6. private void ensureCapacityInternal(int paramInt) {  
  7.     if (this.elementData == EMPTY_ELEMENTDATA)  
  8.         paramInt = Math.max(10, paramInt);  
  9.     ensureExplicitCapacity(paramInt);  
  10. }  
  11.   
  12. private void ensureExplicitCapacity(int paramInt) {  
  13.     this.modCount += 1;    //修改modCount  
  14.     /** 省略此處代碼 */  
  15. }  
  16.   
  17. ublic boolean remove(Object paramObject) {  
  18.     int i;  
  19.     if (paramObject == null)  
  20.         for (i = 0; i < this.size; ++i) {  
  21.             if (this.elementData[i] != null)  
  22.                 continue;  
  23.             fastRemove(i);  
  24.             return true;  
  25.         }  
  26.     else  
  27.         for (i = 0; i < this.size; ++i) {  
  28.             if (!(paramObject.equals(this.elementData[i])))  
  29.                 continue;  
  30.             fastRemove(i);  
  31.             return true;  
  32.         }  
  33.     return false;  
  34. }  
  35.   
  36. private void fastRemove(int paramInt) {  
  37.     this.modCount += 1;   //修改modCount  
  38.     /** 省略此處代碼 */  
  39. }  
  40.   
  41. public void clear() {  
  42.     this.modCount += 1;    //修改modCount  
  43.     /** 省略此處代碼 */  
  44. }  

        從上面的源代碼我們可以看出,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。推薦使用該方案。

        CopyOnWriteArrayList爲何物?ArrayList 的一個線程安全的變體,其中所有可變操作(add、set 等等)都是通過對底層數組進行一次新的複製來實現的。 該類產生的開銷比較大,但是在兩種情況下,它非常適合使用。1:在不能或不想進行同步遍歷,但又需要從併發線程中排除衝突時。2:當遍歷操作的數量大大超過可變操作的數量時。遇到這兩種情況使用CopyOnWriteArrayList來替代ArrayList再適合不過了。那麼爲什麼CopyOnWriterArrayList可以替代ArrayList呢?

        第一、CopyOnWriterArrayList的無論是從數據結構、定義都和ArrayList一樣。它和ArrayList一樣,同樣是實現List接口,底層使用數組實現。在方法上也包含add、remove、clear、iterator等方法。

        第二、CopyOnWriterArrayList根本就不會產生ConcurrentModificationException異常,也就是它使用迭代器完全不會產生fail-fast機制。請看:

[java] view plain copy
 print?
  1. private static class COWIterator<E> implements ListIterator<E> {  
  2.         /** 省略此處代碼 */  
  3.         public E next() {  
  4.             if (!(hasNext()))  
  5.                 throw new NoSuchElementException();  
  6.             return this.snapshot[(this.cursor++)];  
  7.         }  
  8.   
  9.         /** 省略此處代碼 */  
  10.     }  

        CopyOnWriterArrayList的方法根本就沒有像ArrayList中使用checkForComodification方法來判斷expectedModCount 與 modCount 是否相等。它爲什麼會這麼做,憑什麼可以這麼做呢?我們以add方法爲例:

[java] view plain copy
 print?
  1. public boolean add(E paramE) {  
  2.         ReentrantLock localReentrantLock = this.lock;  
  3.         localReentrantLock.lock();  
  4.         try {  
  5.             Object[] arrayOfObject1 = getArray();  
  6.             int i = arrayOfObject1.length;  
  7.             Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
  8.             arrayOfObject2[i] = paramE;  
  9.             setArray(arrayOfObject2);  
  10.             int j = 1;  
  11.             return j;  
  12.         } finally {  
  13.             localReentrantLock.unlock();  
  14.         }  
  15.     }  
  16.   
  17.       
  18.     final void setArray(Object[] paramArrayOfObject) {  
  19.         this.array = paramArrayOfObject;  
  20.     }  

        CopyOnWriterArrayList的add方法與ArrayList的add方法有一個最大的不同點就在於,下面三句代碼:

[java] view plain copy
 print?
  1. Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
  2. arrayOfObject2[i] = paramE;  
  3. setArray(arrayOfObject2);  

        就是這三句代碼使得CopyOnWriterArrayList不會拋ConcurrentModificationException異常。他們所展現的魅力就在於copy原來的array,再在copy數組上進行add操作,這樣做就完全不會影響COWIterator中的array了。

        所以CopyOnWriterArrayList所代表的核心概念就是:任何對array在結構上有所改變的操作(add、remove、clear等),CopyOnWriterArrayList都會copy現有的數據,再在copy的數據上修改,這樣就不會影響COWIterator中的數據了,修改完成之後改變原有數據的引用即可。同時這樣造成的代價就是產生大量的對象,同時數組的copy也是相當有損耗的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章