1. 程式人生 > 其它 >java中的fast-fail機制

java中的fast-fail機制

概念

fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。

 

分析

先看一個程式碼:

 1 public class Test {
 2     private static List<Integer> list = new ArrayList<>();
 3     public static void main(String[] args) {
 4         Thread t1 = new ThreadOne();
 5         Thread t2 = new
ThreadTwo(); 6 t1.start(); 7 t2.start(); 8 } 9 10 static void print(){ 11 Iterator<Integer> iter = list.iterator(); 12 while(iter.hasNext()){ 13 System.out.println(iter.next()); 14 } 15 } 16 17 static class ThreadOne extends
Thread{ 18 @Override 19 public void run(){ 20 for(int i=0; i<3; i++){ 21 list.add(i); 22 print(); 23 } 24 } 25 } 26 27 static class ThreadTwo extends Thread{ 28 @Override 29 public void run(){
30 for(int i=0; i<3; i++){ 31 list.add(i); 32 print(); 33 } 34 } 35 } 36 }

執行結果如下:

 

 

 

可以看到, 出現了ConcurrentModificationException異常,這就是fast-fail機制。

我們都已經知道了,fail-fast快速失敗是在迭代的時候產生的,但是是如何產生的呢?下面我們再來深入的分析一下:

根本原因:

從前面我們知道fail-fast是在操作迭代器時產生的。現在我們來看看ArrayList中迭代器的原始碼:

 1 private class Itr implements Iterator<E> {
 2         int cursor;       // index of next element to return
 3         int lastRet = -1; // index of last element returned; -1 if no such
 4         int expectedModCount = modCount;
 5 
 6         Itr() {}
 7 
 8         public boolean hasNext() {
 9             return cursor != size;
10         }
11 
12         @SuppressWarnings("unchecked")
13         public E next() {
14             checkForComodification();
15             int i = cursor;
16             if (i >= size)
17                 throw new NoSuchElementException();
18             Object[] elementData = ArrayList.this.elementData;
19             if (i >= elementData.length)
20                 throw new ConcurrentModificationException();
21             cursor = i + 1;
22             return (E) elementData[lastRet = i];
23         }
24 
25         public void remove() {
26             if (lastRet < 0)
27                 throw new IllegalStateException();
28             checkForComodification();
29 
30             try {
31                 ArrayList.this.remove(lastRet);
32                 cursor = lastRet;
33                 lastRet = -1;
34                 expectedModCount = modCount;
35             } catch (IndexOutOfBoundsException ex) {
36                 throw new ConcurrentModificationException();
37             }
38         }
1         final void checkForComodification() {
2             if (modCount != expectedModCount)
3                 throw new ConcurrentModificationException();
4         }

 

從上面的原始碼我們可以看出,迭代器在呼叫next()、remove()方法時都是呼叫checkForComodification()方法,它檢測modCount == expectedModCount ? 若不等則丟擲ConcurrentModificationException 異常,從而產生fail-fast機制。

到了這一步我們也知道了,想要弄清楚fail-fast機制,首先我們需要搞清楚modCount 和expectedModCount。

expectedModCount 是在IteratorTest中定義的:

1 int expectedModCount = modCount;

所以他的值是不可能會修改的,所以會變的就是modCount。

modCount是在 AbstractList 中定義的,為全域性變數:

1 protected transient int modCount = 0;

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

 1 public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // clear to let GC do its work
12 
13         return oldValue;
14     }
15 
16 
17 public boolean add(E e) {
18         ensureCapacityInternal(size + 1);  // Increments modCount!!
19         elementData[size++] = e;
20         return true;
21     }v
22 
23 
24 private void ensureCapacityInternal(int minCapacity) {
25         ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
26     }
27 
28 private void ensureExplicitCapacity(int minCapacity) {
29         modCount++;
30 
31         // overflow-conscious code
32         if (minCapacity - elementData.length > 0)
33             grow(minCapacity);
34     }

 

對於ArrayLis來說,只要是涉及了改變ArrayList元素的個數的方法都會導致modCount的改變。所以我們這裡可以判斷由於expectedModCount 與modCount的改變不同步,導致兩者之間不等,從而產生fail-fast機制

 

單執行緒下面的fast-fail機制

事實上,即使是單執行緒下也有可能會出現這種情況。

不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 物件加鎖。

通過反編譯你會發現 foreach 語法糖底層其實還是依賴 Iterator 。不過, remove/add 操作直接呼叫的是集合自己的方法,而不是 Iterator 的 remove/add方法

這就導致 Iterator 莫名其妙地發現自己有元素被 remove/add ,然後,它就會丟擲一個 ConcurrentModificationException 來提示使用者發生了併發修改異常。這就是單執行緒狀態下產生的 fail-fast 機制。

 1 public class Test {
 2     private static List<Integer> list = new ArrayList<>();
 3     public static void main(String[] args) {
 4         test();
 5     }
 6 
 7     private static void test(){
 8         List<Integer> list = new ArrayList<>();
 9         list.add(1);
10         list.add(1);
11         list.add(1);
12         list.add(1);
13         for(Integer i: list){
14             if(i == 1){
15                 list.remove((Object)i);
16             }
17         }
18     }
19 }

 

 

 

解決方案

我們如何去規避這種情況呢?這裡有兩種解決方案:

  • 方案一:在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList(不推薦)
  • 方案二:使用CopyOnWriteArrayList來替換ArrayList。

CopyOnWriteArrayList為什麼能解決這個問題呢?CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡新增元素,新增完元素之後,再將原容器的引用指向新的容器。CopyOnWriteArrayList中add/remove等寫方法是需要加鎖的,目的是為了避免Copy出N個副本出來,導致併發寫。但是。CopyOnWriteArrayList中的讀方法是沒有加鎖的。

我們只需要記住一句話,那就是CopyOnWriteArrayList是執行緒安全的,所以我們在多執行緒的環境下面需要去使用這個就可以了。