【Java深入研究】4、fail-fast機制

来源:http://www.cnblogs.com/wangzhongqiu/archive/2017/08/03/6589183.html
-Advertisement-
Play Games

在JDK的Collection中我們時常會看到類似於這樣的話: 例如,ArrayList: 註意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。因此,為提 ...


在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  
  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  
  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  
  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  
  1. protected transient int modCount = 0;  

 

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

 

[java] view plain copy  
  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  
  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  
  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  
  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也是相當有損耗的。

轉自:http://blog.csdn.net/chenssy/article/details/38151189


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上一篇文章記錄了怎麼安裝Python環境,同時也成功的在電腦上安裝好了Python環境,可以正式開始自己的編程之旅了。但是現在又有頭疼的事情,該用什麼來寫Python程式呢,該用什麼來執行Python程式呢。 其實市面上有很多編輯器都可以來編寫代碼,甚至是Windows自帶的記事本都可以編寫代碼。俗 ...
  • 一、變數 變數命名規則: 1、變數名只能包含字母、數字和下劃線,不能以數字開頭 2、變數名不能包含空格,可以用下劃線來分割單詞 3、不要將Python關鍵字和函數名用作變數,已經被徵用 4、即短有具有描述性 5、慎用小寫字母i和o,容易誤解 二、字元串 字元串是一種數據類型,在Python中用()括 ...
  • 使用Python自帶的函數strip可以剔除字元串開頭、結尾、兩端的空白 使用場景:用戶輸入驗證 strip : 去除字元串兩端的空白 rstrip : 去除字元串末尾(右端)的空白 lstrip : 去除字元串開頭(左端)的空白 示例: ...
  • 學了Java有一段時間了,自認為有一些基礎知識比較重要,因此記下來共用,不喜勿噴。 一、標識符 (1)定義:在Java語言中,凡是對類,方法,變數,包,參數等命名時,所使用的字元序列 (2)包含的內容:0-9、a-z、A-Z、&、_ (3)註意的規則:1.由字母、數字、下劃線和美元符號組成 2.不能 ...
  • 系統自動拋出的異常 所有系統定義的編譯和運行異常都可以由系統自動拋出,稱為標準異常,並且 Java 強烈地要求應用程式進行完整的異常處理,給用戶友好的提示,或者修正後使程式繼續執行。 語句拋出的異常 用戶程式自定義的異常和應用程式特定的異常,必須藉助於 throws 和 throw 語句來定義拋出異 ...
  • package java.util; public interface Enumeration<E> { boolean hasMoreElements(); E nextElement(); } public interface Iterator<E> { boolean hasNext(); E ...
  • 內部類的分類:常規內部類、靜態內部類、私有內部類、局部內部類、匿名內部類。 實例1:常規內部類 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //外部類 class Out { private int age = 12; ...
  • 一、首先說一下JDK中的動態代理: JDK中的動態代理是通過反射類Proxy以及InvocationHandler回調介面實現的 但是,JDK中所要進行動態代理的類必須要實現一個介面,也就是說只能對該類所實現介面中定義的方法進行代理,這在實際編程中具有一定的局限性,而且使用反射的效率也並不是很高。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...