java中的fail-fast(快速失敗)機制
簡介
fail-fast機制,即快速失敗機制,是java集合中的一種錯誤檢測機制。當在迭代集合的過程中對該集合的結構改變是,就有可能會發生fail-fast,即跑出ConcurrentModificationException異常。fail-fast機制並不保證在不同步的修改下一定拋出異常,它只是近最大努力去拋出,所以這種機制一般僅用於檢測bug
fail-fast的出現場景
在我們常見的java集合中就可能出現fail-fast機制,比如常見的ArrayList,HashMap.在多線程和單線程環境下都有可能出現快速失敗。
1.單線程環境下的fail-fast例子:
public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++ ) { list.add(i + ""); } Iterator<String> iterator = list.iterator(); int i = 0 ; while(iterator.hasNext()) { if (i == 3) { list.remove(3); } System.out.println(iterator.next()); i ++; } }
控制台打印:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.example.springboot_demo.fail_fast_safe.ArrayListFailFast.main(ArrayListFailFast.java:24)
該段代碼定義了一個Arraylist集合,並使用迭代器遍歷,在遍歷過程中,刻意在某一步迭代中remove一個元素,這個時候,就會發生fail-fast
2.多線程環境下
public class ArrayListFailFastThreadPool { public static List<String> list = new ArrayList<>(); private static class MyThread1 extends Thread { @Override public void run() { Iterator<String> iterator = list.iterator(); while(iterator.hasNext()) { String s = iterator.next(); System.out.println(this.getName() + ":" + s); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } super.run(); } } private static class MyThread2 extends Thread { int i = 0; @Override public void run() { while (i < 10) { System.out.println("thread2:" + i); if (i == 2) { list.remove(i); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } i ++; } } } public static void main(String[] args) { for(int i = 0 ; i < 10;i++){ list.add(i+""); } MyThread1 thread1 = new MyThread1(); MyThread2 thread2 = new MyThread2(); thread1.setName("thread1"); thread2.setName("thread2"); thread1.start(); thread2.start(); } }
控制台打印:
Exception in thread "thread1" java.util.ConcurrentModificationException thread2:3 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.example.springboot_demo.fail_fast_safe.ArrayListFailFastThreadPool$MyThread1.run(ArrayListFailFastThreadPool.java:20)
啟動兩個線程,分別對其中一個對list進行迭代,另一個在線程1的迭代過程中去remove一個元素,結果也是拋出了java.util.ConcurrentModificationException
Fail-fast原理
fail-fast是如何拋出ConcurrentModificationException異常的,又是在什么情況下才會拋出?
我們知道,對於集合入list,map類,我們都是可以通過迭代器來遍歷,而Iterator其實只是一個接口,具體的實現還是具體的集合類的內部類實現Iterator並實現相關方法。這里我們就以ArrayList類為例。在ArrayList中,當調用list.iterator()時,其源碼是:
public Iterator<E> iterator() { return new Itr(); } private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } //這段代碼是關鍵 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
可以看出,該方法才是判斷是否拋出ConcurrentModificationException異常的關鍵。在該段代碼中,當modCount != expectedModCount時,就會拋出該異常。很明顯expectedModCount在整個迭代過程除了一開始賦予初始值modCount外,並沒有再發生改變,所以可能發生改變的就只有modCount.
在前面關於ArrayList擴容機制的分析中,可以知道在ArrayList進行add,remove,clear等涉及到修改集合中的元素個數的操作時,modCount就會發生改變(modCount++)所以當另一個線程(並發修改)或者同一個線程遍歷過程中,調用相關方法使集合的個數發生改變,就會使modCount發生變化,這樣在checkForComodification方法中就會拋出ConcurrentModificationException異常。
類似的,hashMap中發生的原理也是一樣的。
避免fail-fast
方法1
在單線程的遍歷過程中,如果要進行remove操作,可以調用迭代器的remove方法而不是集合類的remove方法
public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++ ) { list.add(i + ""); } Iterator<String> iterator = list.iterator(); int i = 0 ; while(iterator.hasNext()) { if (i == 3) { iterator.remove(); //迭代器的remove()方法 } System.out.println(iterator.next()); i ++; } }
方法2
使用java並發包(java.util.concurrent)中的類來代替ArrayList 和hashMap。
對於HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了鎖機制,是線程安全的。在迭代方面,ConcurrentHashMap使用了一種不同的迭代方式。在這種迭代方式中,當iterator被創建后集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據 ,iterator完成后再將頭指針替換為新的數據 ,這樣iterator線程可以使用原來老的數據,而寫線程也可以並發的完成改變。即迭代不會發生fail-fast,但不保證獲取的是最新的數據。