fail-fast(快速失败/报错机制)-ConcurrentModificationException
2019獨角獸企業重金招聘Python工程師標準>>>
一、fail-fast機制(快速報錯機制)
這是《Java編程思想》中關于快速報錯機制的描述
Java容器有一種保護機制,能夠防止多個進程同時修改同一個容器的內容。如果在你迭代遍歷容器的過程中,另一個進程介入其中,并且插入、刪除或者修改此容器內的某個對象,那么就會出現問題:也許迭代過程中已經處理過容器中的該元素了,也許還沒處理,也許在調用size()之后容器的尺寸收縮了——還有許多災難情景。Java容器類類庫采用快速報錯(fail-fast)機制。它會探查容器上的任何除了你的進程所進行的操作以外的所有變化,一旦它發現其它進程修改了容器,就會立刻拋出ConcurrentModificationException異常。這就是“快速報錯”的意思——即,不是使用復雜的算法在事后來檢查問題。——from《Java編程思想》p517
下面以一個demo開始,來理解快速報錯機制。
二、遍歷容器的幾種方式
程序功能:分別使用for,foreach,iterator來遍歷(迭代)容器,然后刪除其中的值為”傻強”這個元素。
public class TestTest {private List<String> list;/** * 初始化操作 */@Beforepublic void setUp(){list = new ArrayList<String>();list.add("劉德華"); list.add("周潤發");list.add("傻強");list.add("古天樂");list.add("劉青云");System.out.println(list);}/** * Demo1:使用for循環刪除元素 */@Testpublic void testFor(){ for(int i=0;i<list.size();i++){//刪除傻強if("傻強".equals(list.get(i))){list.remove(i);}}System.out.println(list);}/** * Demo2:使用foreach刪除元素【錯誤】 */@Testpublic void testForeach(){ for (String s : list) {//刪除傻強if("傻強".equals(s)){list.remove(s);}}System.out.println(list);}/** * Demo3:使用Iterator和Iterator的remove()刪除元素 */@Testpublic void testIterator(){Iterator<String> iterator = list.iterator();while(iterator.hasNext()){String s= iterator.next();//刪除lisiif("傻強".equals(s)){iterator.remove();//使用迭代器的remove()}}System.out.println(list);}/** * Demo4:使用Iterator和集合的remove刪除元素【錯誤】 */@Testpublic void testIterator2(){Iterator<String> iterator = list.iterator();while(iterator.hasNext()){String s= iterator.next();//刪除傻強if("傻強".equals(s)){list.remove(s);//使用集合的remove}}System.out.println(list);}/** * Demo5:獲得iterator后進行了錯誤操作【錯誤】 */@Testpublic void testIterator3(){Iterator<String> iterator = list.iterator();//錯誤操作list.add("這是錯誤的行為");while(iterator.hasNext()){String s= iterator.next();//刪除傻強if("傻強".equals(s)){iterator.remove();}}System.out.println(list);} }結果:只有Demo1和Demo3正確。其它demo都會報ConcurrentModificationException異常。這里就用到了fail-fast機制。
三、ArrayList中Iterator源碼分析
接下來開始分析。我們先看看ArrayLis中的關于迭代器的代碼
public Iterator<E> iterator() {return new Itr();}/** * An optimized version of AbstractList.Itr * * 覆蓋了父類中AbstractList.Itr的實現(優化版) */private class Itr implements Iterator<E> {//下一個要返回元素的索引int cursor; // index of next element to return//最后一個要返回元素的索引,-1表示不存在int lastRet = -1; // index of last element returned; -1 if no such//記錄期望的修改次數(用于保證迭代器在遍歷過程中不會有對集合的修改操作(迭代器的自身的remove方法除外))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();}}/** * 檢查修改次數 */final void checkForComodification() {//實際的修改次數和期望的修改次數不匹配,則拋出并發修改異常if (modCount != expectedModCount)throw new ConcurrentModificationException();}}原來,ArrayList從其父類AbstractList繼承了一個modCount屬性,每當對ArrayList進行修改(add,remove,clear等)時,就會相應的增加modCount的值。
而ArrayList中迭代器的實現類Itr也有一個expectedModCount屬性,一旦使用迭代器遍歷容器時,就要調用iterator()方法,Itr類也就被初始化,expectedModCount就會被賦予一個與modCount相等的值。
接下來在遍歷過程中,每次調用next()方法獲取值時都會檢查modCount和expectedModCount兩個值是否相等(checkForComodification)。如果在遍歷過程中出現了對集合的其它修改操作,從而造成兩者不等,就會拋出ConcurrentModificationException。這不就是樂觀鎖的實現思想嗎。
另外,我們注意到迭代器自身的remove方法并不會修改modCount的值,這是因為我們通常也會通過迭代遍歷去刪除某一個指定的元素,所以迭代器中自身提供了該remove方法,并保證該remove方法是安全的,而不希望我們在迭代時使用容器提供remove方法。
四、避免fail-fast
要說明兩點
1.雖然ConcurrentModificationException被譯為并發修改異常,但這里的”并發”,并非僅僅指的是多線程場景,前面的例子很顯然是單線程場景。
在單線程情況下
要確保Iterator遍歷過程順利完成,必須保證遍歷過程中不更改集合的內容(Iterator的remove()方法除外)。
多線程情況下
如果要在多線程環境中,在迭代ArrayList的同時也要修改ArrayList,則可以使用
Collections.synchronizedList(List list)或者CopyOnWriteArrayList。
其中CopyOnWriteArrayList是可以避免ConcurrentModificationException。
實際上CopyOnWriteArrayList、ConcurrentHashMap和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技術。
2.迭代器的快速失敗行為無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出ConcurrentModificationException異常。因此,為提高此類操作的正確性,我們不能依賴于此異常,而要使用上一條中提到的線程安全的容器。
五、CopyOnWriteArrayList不使用fail-fast機制
通過上面的分析,我么知道了ArrayList一邊使用迭代器遍歷一邊修改是會發生ConcurrentModificationException。但是,ArrayList對應的線程安全容器CopyOnWriteArrayList卻不會發生ConcurrentModificationException。那是為什么呢?先來看源碼。
/** * 返回迭代器 * * 返回的迭代器提供了該迭代器被創建時列表的快照。 * 當移動迭代器時,不需要同步。 * 迭代器不支持remove方法 */public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);}/** * 內部迭代器的實現類 */private static class COWIterator<E> implements ListIterator<E> {/**數組的快照*/private final Object[] snapshot;/** Index of element to be returned by subsequent call to next. */private int cursor;/** * 私有的構造器 */private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;//快照snapshot = elements;}public boolean hasNext() {return cursor < snapshot.length;}public boolean hasPrevious() {return cursor > 0;}@SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}@SuppressWarnings("unchecked")public E previous() {if (! hasPrevious())throw new NoSuchElementException();return (E) snapshot[--cursor];}public int nextIndex() {return cursor;}public int previousIndex() {return cursor-1;}/** * Not supported. Always throws UnsupportedOperationException. * remove is not supported by this iterator. * 不支持。總是拋出不支持的操作異常 * 迭代器不支持remove方法。 */public void remove() {throw new UnsupportedOperationException();}/** * Not supported. Always throws UnsupportedOperationException. * set is not supported by this iterator. * 不支持。總是拋出不支持的操作異常 * 迭代器不支持set方法。 * */public void set(E e) {throw new UnsupportedOperationException();}/** * Not supported. Always throws UnsupportedOperationException. * add is not supported by this iterator. * 不支持。總是拋出不支持的操作異常 * 迭代器不支持aa方法。 * */public void add(E e) {throw new UnsupportedOperationException();}}原來,CopyOnWriteArrayList中的迭代器在創建之初,會保存一份對原數組的快照,之后所有迭代器的操作都是在快照數組上進行的,原數組一點影響都沒有。同時,即使是對快照數組進行操作,也根本不支持迭代器上的修改(add,set,remove)操作。因此,根本就不會發生ConcurrentModificationException。
雖然在迭代時不支持迭代器上的修改操作,但是仍然可以直接使用容器的修改方法,這點恰好跟ArrayList相反。這也很容易解釋,因為迭代器操作的是快照數組,在原容器上進行修改也是會創建一個新的數組,因此兩者根本不會干擾。
public class Demo {public static void main(String[] args) {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();//添加元素0-4for(int i=0;i<5;i++){list.add(i+"");}System.out.println(list);//[0, 1, 2, 3, 4]//進行迭代Iterator iterator = list.iterator();while(iterator.hasNext()){String num = (String) iterator.next();//迭代時,刪除3if("3".equals(num)){//iterator.remove();//iterator不支持修改方法(add,set,remove)list.remove(num);//使用原容器的remove方法};}System.out.println(list);//[0, 1, 2, 4]} }轉載于:https://my.oschina.net/javandroid/blog/878235
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的fail-fast(快速失败/报错机制)-ConcurrentModificationException的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: struts2的DevMode(开发模式
- 下一篇: Spring MVC 环境搭建(一)