java.util.ConcurrentModificationException 出現的原因和解決辦法
用iterator遍歷集合時碰到java.util.ConcurrentModificationException這個異常,
下面以List為例來解釋為什麼會報java.util.ConcurrentModificationException這個異常,程式碼如下:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list .add("3");
list.add("4");
list.add("5");
list.add("6");
list.add("7");
List<String> del = new ArrayList<String>();
del.add("5");
del.add("6");
del.add("7");
for(String str : list){
if(del.contains(str)) {
list .remove(str);
}
}
}
執行這段程式碼會出現如下異常:
Exception in thread "main" java.util.ConcurrentModificationException
for(String str : list) 這句話實際上是用到了集合的iterator() 方法
JDK java.util. AbstractList類中相關原始碼
public Iterator<E> iterator() {
return new Itr();
}
java.util. AbstractList的內部類Itr的原始碼如下:
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification(); //檢測modCount和expectedModCount的值!!
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet); //執行remove的操作
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount; //保證了modCount和expectedModCount的值的一致性,避免丟擲ConcurrentModificationException異常
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則丟擲ConcurrentModificationException異常
throw new ConcurrentModificationException();
}
}
再看一下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 remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++; //只是修改了modCount,因此modCount將與expectedModCount的值不一致
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
回過頭去看看java.util. AbstractList的next()方法
public E next() {
checkForComodification(); //檢測modCount和expectedModCount的值!!
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則丟擲ConcurrentModificationException異常
throw new ConcurrentModificationException();
}
}
現在真相終於大白了,ArrayList的remove方法只是修改了modCount的值,並沒有修改expectedModCount,導致modCount和expectedModCount的值的不一致性,當next()時則丟擲ConcurrentModificationException異常
因此使用Iterator遍歷集合時,不要改動被迭代的物件,可以使用 Iterator 本身的方法 remove() 來刪除物件, Iterator.remove() 方法會在刪除當前迭代物件的同時維護modCount和expectedModCount值的一致性。
解決辦法如下:
(1) 新建一個集合存放要刪除的物件,等遍歷完後,呼叫removeAll(Collection
List<String> save = new ArrayList<String>();
for(String str : list)
{
if(del.contains(str))
{
save.add(str);
}
}
list.removeAll(save);
(2) 使用Iterator替代增強型for迴圈:
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String str = iterator.next();
if(del.contains(str)) {
iterator.remove();
}
}
Iterator.remove()方法保證了modCount和expectedModCount的值的一致性,避免丟擲ConcurrentModificationException異常。
不過對於在多執行緒環境下對集合類元素進行迭代修改操作,最好把程式碼放在一個同步程式碼塊內,這樣才能保證modCount和expectedModCount的值的一致性,類似如下:
Iterator<String> iterator = list.iterator();
synchronized(synObject) {
while(iterator.hasNext()) {
String str = iterator.next();
if(del.contains(str)) {
iterator.remove();
}
}
}
因為迭代器實現類如:ListItr的next(),previous(),remove(),set(E e),add(E e)這些方法都會呼叫checkForComodification(),原始碼:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
曾經寫了下面這段對HashMap進行迭代刪除操作的錯誤的程式碼:
Iterator<Integer> iterator = windows.keySet().iterator();
while(iterator.hasNext()) {
int type = iterator.next();
windows.get(type).closeWindow();
iterator.remove();
windows.remove(type); //
}
上面的程式碼也會導致ConcurrentModificationException的發生。罪魁禍首是windows.remove(type);這一句。
根據上面的分析我們知道iterator.remove();會維護modCount和expectedModCount的值的一致性,而windows.remove(type);這句是不會的。其實這句是多餘的,上面的程式碼去掉這句就行了。
iterator.remove()的原始碼如下:HashIterator類的remove()方法
public void remove() {
if (lastEntryReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
HashMap.this.remove(lastEntryReturned.key);
lastEntryReturned = null;
expectedModCount = modCount; //保證了這兩值的一致性
}
HashMap.this.remove(lastEntryReturned.key);這句程式碼說明windows.remove(type);是多餘的,因為已經刪除了該key對應的value。
windows.remove(type)的原始碼:
public V remove(Object key) {
if (key == null) {
return removeNullKey();
}
int hash = secondaryHash(key.hashCode());
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index], prev = null;
e != null; prev = e, e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
if (prev == null) {
tab[index] = e.next;
} else {
prev.next = e.next;
}
modCount++;
size--;
postRemove(e);
return e.value;
}
}
return null;
}
上面的程式碼中,由於先呼叫了iterator.remove();所以再呼叫HashMap的remove方法時,key就已經為null了,所以會執行:removeNullKey();
方法,removeNullKey()原始碼:
private V removeNullKey() {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null) {
return null;
}
entryForNullKey = null;
modCount++;
size--;
postRemove(e);
return e.value;
}
不過不管是執行removeNullKey()還是key != null,如果直接呼叫HashMap的remove方法,都會導致ConcurrentModificationException
這個異常的發生,因為它對modCount++;沒有改變expectedModCount的值,沒有維護維護索引的一致性。
下面引用一段更專業的解釋:
Iterator 是工作在一個獨立的執行緒中,並且擁有一個 mutex 鎖。 Iterator 被建立之後會建立一個指向原來物件的單鏈索引表,當原來的物件數量發生變化時,這個索引表的內容不會同步改變,所以當索引指標往後移動的時候就找不到要迭代的物件,所以按照 fail-fast 原則 Iterator 會馬上丟擲 java.util.ConcurrentModificationException 異常。
所以 Iterator 在工作的時候是不允許被迭代的物件被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除物件, Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。