fail-fast和fail-safe詳解
一:快速失敗(fail—fast)
在用迭代器遍歷一個集合物件時,如果遍歷過程中對集合物件的內容進行了修改(增加、刪除、修改),則會丟擲Concurrent Modification Exception。
原理:迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變數。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變數是否為expectedmodCount值,是的話就返回遍歷;否則丟擲異常,終止遍歷。
注意:這裡異常的丟擲條件是檢測到 modCount!=expectedmodCount 這個條件。如果集合發生變化時修改modCount值剛好又設定為了expectedmodCount值,則異常不會丟擲。因此,不能依賴於這個異常是否丟擲而進行併發操作的程式設計,這個異常只建議用於檢測併發修改的bug。
場景:java.util包下的集合類都是快速失敗的,不能在多執行緒下發生併發修改(迭代過程中被修改)。
如下程式碼會發生Concurrent Modification Exception:
package test; import java.util.*; import java.util.concurrent.*; 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++; } } } }
執行結果:
執行該程式碼,丟擲異常java.util.ConcurrentModificationException!即,產生fail-fast事件!
結果說明:
1. FastFailTest中通過 new ThreadOne().start() 和 new ThreadTwo().start() 同時啟動兩個執行緒去操作list。
ThreadOne執行緒:向list中依次新增0,1,2,3,4,5。每新增一個數之後,就通過printAll()遍歷整個list。
ThreadTwo執行緒:向list中依次新增10,11,12,13,14,15。每新增一個數之後,就通過printAll()遍歷整個list。
2. 當某一個執行緒遍歷list的過程中,list的內容被另外一個執行緒所改變了;就會丟擲ConcurrentModificationException異常,產生fail-fast事件。
解決方法:
fail-fast機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,因為JDK並不保證fail-fast機制一定會發生。若在多執行緒環境下使用fail-fast機制的集合,建議使用“java.util.concurrent包下的類”去取代“java.util包下的類”。
所以,本例中只需要將ArrayList替換成java.util.concurrent包下對應的類即可。
即,將程式碼
private static List<String> list = new ArrayList<String>();
替換為
private static List<String> list = new CopyOnWriteArrayList<String>();
則可以解決該辦法。
二:安全失敗(fail—safe)
採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。
原理:由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發Concurrent Modification Exception。
缺點:基於拷貝內容的優點是避免了Concurrent Modification Exception,但同樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。
場景:java.util.concurrent包下的容器都是安全失敗,可以在多執行緒下併發使用,併發修改。