一、前言
Java中,集合類ArrayList不管是在開發工作中,還是在面試中,都應該是個比較高頻出現的知識點。在使用過程中,可能會遇到迭代刪除的需求場景,此時如果代碼書寫不當,極有可能會拋出 java.util.ConcurrentModificationException 異常信息。下面對這個異常做點分析,為什么會出現異常,怎樣去正確的迭代刪除。
二、異常原因分析
測試代碼如下:
package com.cfang.prebo.oTest;
import java.util.Iterator;
import java.util.List;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
public class TestListException {
public static void main(String[] args) {
List<Integer> list = Lists.newArrayList();
list.add(1);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer val = iterator.next();
if(val == 1) {
list.remove(val);
// iterator.remove();
}
}
System.out.println("result:" + JSON.toJSONString(list));
}
}
運行結果:

從異常棧信息中可以看出,最終拋出此異常的方法,是 checkForComodification 方法。下面進行追根逐源的看看,為什么方法會拋出異常。
首先整體貼出迭代器 Iterator 對 List 進行迭代的關鍵性代碼片段:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
對List的迭代iterator會new出個Itr對象的引用,Itr是個成員內部類實現。其中幾個關鍵性的屬性:
cursor - 游標索引,表示下一個可訪問的元素的索引
lastRet - 還是索引,是上一個元素的索引。默認值-1
expectedModCount - 對集合的修改期望值,初始值等於modCount
modCount的定義在AbstractList中,初始值為0,如下定義:
protected transient int modCount = 0;
該值會在List的方法add以及remove中,進行加1操作,如下代碼片段:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
也就是說,在進行add和remove的時候,都會將modCount值修改。
好了,鋪墊到這里,就可以來結合測試main方法來進行一步步的解釋說明了:
1、初始化ArrayList,調用list.add方法,此時,modCount=1,list.size = 1
2、初始化itreator迭代循環。此時,expectedModCount = modCount = 1,cursor默認值0,lastRet默認值-1
3、itreator.hasNext方法判斷,cursor != size成立,有元素可訪問,進入循環
4、調用itreator.next方法,校驗后獲取值。此時expectedModCount == modCount成立,校驗通過。獲取值並設置相關屬性 lastRet = 0,cursor = 1
5、調用list.remove方法,modCount加1。此時,modCount=2,list.size = 0
6、itreator.hasNext方法判斷,cursor != size成立,進入循環
7、調用itreator.next方法,校驗方法checkForComodification,此時,expectedModCount != modCount成立,拋出ConcurrentModificationException異常
寫到這里,基本上為啥會出現異常,應該是已經非常明了清晰了。總結起來就是:如果是使用list.remove的話,會導致expectedModCount != modCount條件成立,也即兩個的值會不等。當然了,使用for-each迭代也是一樣的,畢竟for-each底層如果是對集合遍歷的話,也還是利用itreator去做的。
說完原因呢,下面簡單說說解決辦法:
單線程情況下:可以使用迭代器itreator提供的remove,從源碼中可以看出,在方法中會對cursor、lastRet重設值,將expectedModCount重新設值為modCount。
多線程情況下:1、迭代刪除使用鎖 - synchronized或者lock
2、創建安全的容器 - Collections.synchronizedList方法、CopyOnWriteArrayList
