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 = newThreadTwo(); 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 extendsThread{ 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是執行緒安全的,所以我們在多執行緒的環境下面需要去使用這個就可以了。