java併發修改異常
迭代器是依賴於集合而存在的,在判斷成功後,集合的中新添加了元素,而迭代器卻不知道,所以就報錯了,這個錯叫併發修改異常。
一:問題程式碼:
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class Test { public static void main(String[] args) { Collection<String> c=new ArrayList<String>(); c.add("a"); c.add("b"); c.add("c"); Iterator<String> itr=c.iterator(); while(itr.hasNext()){ //迭代器遍歷,但是下面卻用了集合的remove方法 String str= (String)itr.next(); if(str.equals("a")) c.remove("b"); } } }
報錯資訊如下:
二:問題分析
問題出現場景:ConcurrentModificationException:當方法檢測到物件的併發修改,但不允許這種修改時,丟擲此異常。
換句話說就是:迭代器遍歷的時候只能用迭代器的remove()函式,如果用集合物件的remove函式就會爆出併發修改異常。
我們選擇一步一步檢視原始碼:我們想著直接查checkForComodification()函式,但是不行。所以我們先找到ArrayList,然而ArrayList類中根本沒有迭代器的實現,所以我猜測Iterator大概是在他的父類或者介面中。
不出所料:在AbstractList類內果然發現了迭代器的實現函式,程式碼很簡短,從這段程式碼可以看出返回的是一個指向Itr型別物件的引用,但是又出現了我們不認識的Itr(),所以接著去找
//下面是AbstractList的程式碼,只保留了內部類Itr和部分有用的程式碼。下面會拆開詳細講 public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { public Iterator<E> iterator() { return new Itr(); } //可以看出Itr就是AbstractList的一個成員內部類 private class Itr implements Iterator<E> { int expectedModCount = modCount; public boolean hasNext() { return cursor != size(); } public E next() { checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(e); } } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } //這就是最開始報錯資訊類的那個”罪魁禍首“函式,可以看出它的作用就是判斷兩個變數是否相等,那這個兩個變數是什麼呢?下面馬上就講。 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
這裡先講兩個變數:
expectedModCount:這個變數是AbstractList裡面的內部類ltr中的變數。代表預期的修改次數,通過public Iterator<E> iterator() { return new Itr();}這個函式的時候會自動new一個Itr物件,同時會用modCount為其初始化。
int expectedModCount = modCount;
modCount:這個是AbstractList類中的變數。表示實際的修改次數。
protected transient int modCount = 0;
該值表示對List的修改次數,檢視ArrayList的add()和remove()方法就可以發現,每次呼叫add()方法或者remove()方法就會對modCount進行加1操作。
好了,到這裡我們再回到最初的程式:
當呼叫c.iterator()返回一個Iterator之後,通過Iterator的hashNext()方法判斷是否還有元素未被訪問,我們看一下hasNext()方法,hashNext()方法的實現很簡單:
public boolean hasNext() {
return cursor != size();
}
如果下一個訪問的元素下標不等於ArrayList的大小,就表示有元素需要訪問,這個很容易理解,如果下一個訪問元素的下標等於ArrayList的大小,則肯定到達末尾了。
然後通過Iterator的next()方法獲取到下標為0的元素,我們看一下next()方法的具體實現:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
這裡是非常關鍵的地方:首先在next()方法中會呼叫checkForComodification()方法,然後根據cursor的值獲取到元素,接著將cursor的值賦給lastRet,並對cursor的值進行加1操作。
初始時,cursor為0,lastRet為-1,那麼呼叫一次之後,cursor的值為1,lastRet的值為0。
注意此時,modCount為0,expectedModCount也為0。
接著往下看上面的出錯程式,程式中判斷當前元素的值是否為a,若為a,則呼叫物件中的remove()方法(而不是迭代器中的remove函式)來刪除該元素。
那我們先看一下在ArrayList中的remove()方法做了什麼:
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) {
modCount++; //注意這一步操作。其他的都可先不看
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
通過remove方法刪除元素最終是呼叫的fastRemove()方法,在fastRemove()方法中,首先對modCount進行加1操作(因為對集合修改了一次),然後接下來就是刪除元素的操作,最後將size進行減1操作,並將引用置為null以方便垃圾收集器進行回收工作。
那麼注意此時各個變數的值:對於物件c的迭代器itr,其expectedModCount為0,cursor的值為1,lastRet的值為0。
對於物件c本身,其modCount為1,size為0。
接著看程式程式碼,執行完相應操作後,經過判斷繼續while迴圈,呼叫hasNext方法()判斷,然後繼續呼叫迭代器itr的next()方法:
注意,此時要注意next()方法中的第一句:checkForComodification()。
在checkForComodification方法中進行的操作是:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
很顯然,此時modCount為1,而expectedModCount為0,modCount不等於expectedModCount,因此程式就丟擲了ConcurrentModificationException異常。
錯誤是在第一次while迴圈remove函式呼叫完埋下的,是第二次while迴圈next()函式時爆出來的。
關鍵點就在於:呼叫list.remove()方法導致modCount和expectedModCount的值不一致。也就是AbstractList的內部類中的Itr內部類只初始化一次,如果初始化之後再跳出內部類進行其他操作的話,modCount的值就可能改變,但是exceptedModCount的值卻無法同步改變了。這就出錯了,而且併發修改異常也就很容易理解為什麼要這樣叫了。
注意,像使用for-each進行迭代實際上也會出現這種問題。
三:問題解決。
1:單執行緒下
上面以及說過了:迭代器遍歷的時候用集合的函式修改就會丟擲併發修改異常。
修改方法1.迭代器遍歷,呼叫迭代器函式進行操作。
Iterator<String> itr=c.iterator();
//1.迭代器遍歷,呼叫迭代器的操作函式
while(itr.hasNext()){
String str= (String)itr.next();
if(str.equals("a"))
itr.remove();
}
修改方法2.傳統的物件for迴圈遍歷,呼叫物件函式進行操作。
//2.選用普通的for迴圈遍歷,呼叫物件的操作函式
for(int i=0;i<c.size();i++){
//這裡必須進行一個強制的型別轉換,取出來的是Object型別的物件,你得轉換成String
String str= ((ArrayList<String>) c).get(i);
if(str.equals("a"))
c.remove("a");
//一個小細節,iterator裡的remove函式不需要傳參,物件的需要傳參
}
總之就是兩條線路各走各的,不能交叉。
2:多執行緒下
多執行緒下的見下面的參考網址:https://www.cnblogs.com/bsjl/p/7676209.html