4,fail-fast错误机制
一,fail-fast簡介
在JDK的Collection中我們時常會看到類似于這樣的話:
| ArrayList | 注意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現(xiàn)不同步并發(fā)修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。因此,為提高這類迭代器的正確性而編寫一個依賴于此異常的程序是錯誤的做法:迭代器的快速失敗行為應該僅用于檢測 bug。 |
| HashMap | 注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的并發(fā)修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴于此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用于檢測程序錯誤。 |
在這兩段話中反復地提到"快速失敗"。那么何為"快速失敗"機制呢?
fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
二,fail-fast示例
單線程中:
public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<Integer>();list.add(2);Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){//報錯地方Integer integer = iterator.next();if(integer == 2)list.remove(integer);} }
運行結果:
多線程中:
public class Fail_Fast {private static List<Integer> list = new ArrayList<Integer>();//線程1,iterator遍歷集合private static class thread1 extends Thread{public void run() {Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){//報錯地方int i = iterator.next();System.out.println("Thread1 遍歷:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}//線程2,滿足條件時候修改集合結構private static class thread2 extends Thread{ public void run(){int i = 0;while(i < 10){System.out.println("Thread2 run:" + i);if(i == 4)list.remove(i);//刪除一個元素。i++;}}}public static void main(String[] args) { for(int i = 0 ; i < 10;i++){list.add(i);}new thread1().start();new thread2().start();} }
運行結果:
初步知道fail-fast產生的原因就在于程序在對 collection 進行迭代時,某個線程對該 collection 在結構上對其做了修改,這時迭代器就會拋出ConcurrentModificationException 異常信息,從而產生 fail-fast。
三,ConcurrentModificationException異常出現(xiàn)的原因
ConcurrentModificationException 異常指的是:當方法檢測到對象的并發(fā)修改,但不允許這種修改時就拋出該異常。同時需要注意的是,該異常不會始終指出對象已經由不同線程并發(fā)修改,如果單線程違反了規(guī)則,同樣也有可能會拋出改異常。
現(xiàn)在來看看ArrayList中迭代器的源代碼:
首先看ArrayList的iterator()方法的具體實現(xiàn),查看源碼發(fā)現(xiàn)在ArrayList的源碼中并沒有iterator()這個方法,那么很顯然這個方法應該是其父類或者實現(xiàn)的接口中的方法,我們在其父類AbstractList中找到了iterator()方法的具體實現(xiàn),下面是其實現(xiàn)代碼:
private class Itr implements Iterator<E> {int cursor; // 表示下一個要訪問的元素的索引int lastRet = -1; // 表示上一個訪問的元素的索引int expectedModCount = modCount; //表示對ArrayList修改次數(shù)的期望值,它的初始值為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();}} }?
從上面的源代碼我們可以看出,迭代器在調用next()、remove()方法時都是調用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount 是否相等,若不等則拋出ConcurrentModificationException 異常,從而產生fail-fast機制。
expectedModCount 是在Itr中定義的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能會修改的。
注意這個modCount變量,modCount是AbstractList類中的一個成員變量,該值表示對List的修改次數(shù):
查看ArrayList的源碼:
public boolean add(E e) {ensureCapacityInternal(size + 1); elementData[size++] = e;return true; } // 將e添加到ArrayList的指定位置 public void add(int index, E element) {//判斷下標是否數(shù)組越界 rangeCheckForAdd(index);ensureCapacityInternal(size + 1); System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++; } private void ensureCapacityInternal(int minCapacity) {//判斷元素數(shù)組是否為空數(shù)組if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 取較大值minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) {//修改次數(shù)+1modCount++;if (minCapacity - elementData.length > 0)grow(minCapacity); } public E remove(int index) {// 檢查索引是否合法 rangeCheck(index);// 修改次數(shù)+1modCount++;E oldValue = elementData(index);// 需要移動的元素的個數(shù)int numMoved = size - index - 1;if (numMoved > 0)//復制數(shù)組System.arraycopy(elementData, index+1, elementData, index,numMoved);// 賦值為空,有利于進行GC(垃圾回收),避免內存泄漏(否則實際上數(shù)組中依然有該引用,gc無法進行垃圾回收)elementData[--size] = null; // 返回舊值return oldValue; } //該方法能夠擦除list中值為null的元素! public boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false; } private void fastRemove(int index) {// 修改次數(shù)+1modCount++;// 需要移動的元素的個數(shù)int numMoved = size - index - 1;if (numMoved > 0)//復制數(shù)組System.arraycopy(elementData, index+1, elementData, index,numMoved);// 賦值為空,有利于進行GC(垃圾回收),避免內存泄漏(否則實際上數(shù)組中依然有該引用,gc無法進行垃圾回收)elementData[--size] = null; } //清空數(shù)組,把每一個值設為null,方便垃圾回收(不同于reset,數(shù)組默認大小有改變的話不會重置) public void clear() {modCount++;for (int i = 0; i < size; i++) elementData[i] = null;size = 0; }
從上面的源代碼我們可以看出,ArrayList中無論add、remove、clear方法只要是涉及了改變ArrayList元素的個數(shù)的方法都會導致modCount的改變。這里可以初步判斷由于expectedModCount 得值與modCount的改變不同步,導致兩者之間不等從而產生fail-fast機制。
如何證明:
回到我們前面單線程的代碼上:
public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<Integer>();list.add(2);Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){//報錯地方Integer integer = iterator.next();if(integer == 2)list.remove(integer);} }
第一步:第一次while循環(huán)
調用list.iterator()返回一個Iterator之后,通過Iterator的hashNext()方法判斷是否還有元素未被訪問,看一下hasNext()方法:
如果下一個訪問的元素下標不等于ArrayList的大小,就表示有元素需要訪問,如果下一個訪問元素的下標等于ArrayList的大小,則肯定到達末尾了。
再通過iterator.next()方法獲取到下標為0的元素,我們看一下next()方法的具體實現(xiàn):
@SuppressWarnings("unchecked")
public E next() {checkForComodification();int i = cursor;//當下標大于等于List的實際長度時,拋出NoSuchElementExceptionif (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;//當下標大于等于elementData數(shù)組長度時,拋出ConcurrentModificationExceptionif (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;return (E) elementData[lastRet = i];
} checkForComodification()方法判斷modCount==?expectedModCount,顯然這時候相等,然后對cursor的值進行加1操作(初始時,cursor為0),返回具體的元素。注意此時,modCount=0,expectedModCount=0。
接著往下看,程序中判斷當前元素的值是否為2,若為2,則調用list.remove()方法來刪除該元素。remove()方法源碼如下:
//該方法能夠擦除list中值為null的元素! public boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false; } private void fastRemove(int index) {// 修改次數(shù)+1modCount++;// 需要移動的元素的個數(shù)int numMoved = size - index - 1;if (numMoved > 0)//復制數(shù)組System.arraycopy(elementData, index+1, elementData, index,numMoved);// 賦值為空,有利于進行GC(垃圾回收),避免內存泄漏(否則實際上數(shù)組中依然有該引用,gc無法進行垃圾回收)elementData[--size] = null; }
通過remove方法刪除元素最終是調用的fastRemove()方法,在fastRemove()方法中,首先對modCount進行加1操作(表示對集合修改了一次),然后接下來就是刪除元素的操作,最后將size進行減1操作,并將引用置為null以方便垃圾收集器進行回收工作。
注意此時各個變量的值:
| Iterator | 其expectedModCount為0,cursor的值為1。 |
| list | modCount為1(修改了一次),size為0(刪除了一個為2的元素)。 |
第二步:第二次while循環(huán)
調用hasNext()方法判斷,由于此時cursor=1,而size=0,那么返回true,所以繼續(xù)執(zhí)行while循環(huán),然后繼續(xù)調用iterator的next()方法:
在next()方法中會調用checkForComodification()方法,用于判斷modCount==?expectedModCount,如果不等于,則拋出ConcurrentModificationException異常。方法如下:
很顯然,此時modCount=1,而expectedModCount=0,因此程序就拋出了ConcurrentModificationException異常。到這里,想必大家應該明白為何上述代碼會拋出ConcurrentModificationException異常了。關鍵點就在于:調用list.remove()方法導致modCount和expectedModCount的值不一致。
四,如何避免fail-fast錯誤
1,在單線程環(huán)境下避免
其實在Itr類中給出了一個remove()方法:
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();} }
在這個方法中,刪除元素實際上調用的就是list.remove()方法,但是它多了一個操:expectedModCount = modCount;
因此,在迭代器中如果要刪除元素的話,需要調用Itr類的remove方法。更改后的代碼如下:
public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<Integer>();list.add(1);list.add(2);Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){Integer integer = iterator.next();if(integer == 2)iterator.remove();} }
2,在多線程環(huán)境下避免
上面的解決辦法在單線程環(huán)境下適用,但是在多線程下可就不好使,例如:
public class Fail_Fast {private static List<Integer> list = new ArrayList<Integer>();//線程1,iterator遍歷集合private static class thread1 extends Thread{public void run() {Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){int i = iterator.next();System.out.println("Thread1 遍歷:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}//線程2,滿足條件時候修改集合結構private static class thread2 extends Thread{ public void run(){Iterator<Integer> iterator = list.iterator();while(iterator.hasNext()){int i = iterator.next();System.out.println("Thread2 遍歷:" + i);if(i == 2){iterator.remove();}}}}public static void main(String[] args) { for(int i = 0 ; i < 5;i++){list.add(i);}new thread1().start();new thread2().start();} }
運行結果:
多線程中一般有2種解決辦法:
1)在使用iterator迭代的時候使用synchronized或者直接使用Collections.synchronizedList()。
2)使用并發(fā)容器CopyOnWriteArrayList代替ArrayList和Vector。
什么是CopyOnWriteArrayList
CopyOnWriteArrayList是ArrayList 的一個線程安全的變體,其中所有可變操作(add、set 等等)都是通過對底層數(shù)組進行一次新的復制來實現(xiàn)的。 該類產生的開銷比較大。
但是在兩種情況下,它非常適合使用:
1、在不能或不想進行同步遍歷,但又需要從并發(fā)線程中排除沖突時。
2、當遍歷操作的數(shù)量大大超過可變操作的數(shù)量時。遇到這兩種情況使用CopyOnWriteArrayList來替代ArrayList再適合不過了。
那么為什么CopyOnWriterArrayList可以替代ArrayList呢?
1、CopyOnWriterArrayList的無論是從數(shù)據(jù)結構、定義都和ArrayList一樣。它和ArrayList一樣,同樣是實現(xiàn)List接口,底層使用數(shù)組實現(xiàn)。在方法上也包含add、remove、clear、iterator等方法。
2、CopyOnWriterArrayList根本就不會產生ConcurrentModificationException異常,也就是它使用迭代器完全不會產生fail-fast機制。請看內部的迭代器的實現(xiàn):
public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0); }static final class COWIterator<E> implements ListIterator<E> {/** 省略此處代碼 */ @SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}/** 省略此處代碼 */ }
CopyOnWriteArrayList的add()方法:
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;//不同點Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();} }
這三句代碼使得CopyOnWriterArrayList不會拋ConcurrentModificationException異常。首先copy原來的array,再在copy數(shù)組上進行add操作,這樣做就完全不會影響COWIterator中的array了。
CopyOnWriterArrayList所代表的核心概念就是:任何對array在結構上有所改變的操作(add、remove、clear等),CopyOnWriterArrayList都會copy現(xiàn)有的數(shù)據(jù),再在copy的數(shù)據(jù)上修改,這樣就不會影響COWIterator中的數(shù)據(jù)了,修改完成之后改變原有數(shù)據(jù)的引用即可。同時這樣造成的代價就是產生大量的對象,同時數(shù)組的copy也是相當有損耗的。
轉載于:https://www.cnblogs.com/Zender/p/8119263.html
總結
以上是生活随笔為你收集整理的4,fail-fast错误机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有没有人知道这个妹子演过什么电影?
- 下一篇: 洱海允许在海边搭帐篷吗