java -- 迭代器并发修改异常

问题描述

我们在使用iterator对象遍历集合时,时常会遇到ConcurrentModificationException这类的错误

例如:

1
2
3
4
5
6
7
8
9
10
11
12
Collection c = new ArrayList();

c.add("I");
c.add("love");
c.add("java");

Iterator<String> it = c.iterator();
while (it.hasNext()){
if(it.next().equals("love"))
c.add("Oracle"); //在使用迭代器遍历集合时,修改了原集合

}

输出为

1
2
3
4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at IteratorTest.main(IteratorTest.java:15)

这主要是因为,迭代器遍历集合,实际上是对集合的副本进行操作;这期间,如果迭代器发现自己和集合不一样了,就会报ConcurrentModificationException异常。

原因 – 迭代器的并发检测

我们可以从JDK源码的获知,当程序执行到it.next(),实际上调用了如下代码:

1
2
3
4
5
6
7
8
9
10
11
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];
}

对于第一个函数checkForComodification(),其源码如下:

1
2
3
4
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

这个方法主要是通过比较modConutexpectedModCount是否相等,来判断迭代器运行期间,原集合是否发生了变化。其中modCountArrayList的类成员变量,用来记录其变化次数;而expectedModCount作为迭代器成员变量,则存储了iterator初始化时记录到的ArrayList中的modCount值。两者相比,即可判断集合是否在迭代器操作期间发生了变化。

解决方法

为了解决这个并发修改异常,最直接的方法就是在iterator运行期间,不要修改原集合。但万一我们业务需要,必须得修改,又该怎么办呢?

既然不允许我们直接修改原集合,那么我们可以考虑通过迭代器去间接操作原集合。通过查询API文档,我们在Iterator接口中并未发现add()方法。既然根类Iterator中没有,我们就到其子接口中去寻找。不出所料,在其Iterator子接口ListIterator中,我们发现了add()方法。首先看一下它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void add(E e) {
checkForComodification();

try {
int i = cursor;
ArrayList.this.add(i, e); //调用了ArrayList当前对象的成员方法
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount; //这里将expectedModCount与modCount进行了同步
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

从源码中,我们可以看到,虽然仍然存在并发检测函数checkForComodification(),但add()在内部为我们完成了集合元素的添加,最最关键的是它还进行了expectedModCountmodCount同步操作,这就保证了并发修改异常不会被触发。

终上所述,最开始的问题代码可以修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//这里将c的引用类型从Collection换成List,是因为Collection接口中没有listIterator()方法,多态无法调用父接口中没有的方法   
List c = new ArrayList();

c.add("I");
c.add("love");
c.add("java");

ListIterator<String> it = c.listIterator();//理由同上,Iterator中没有add方法
while (it.hasNext()){
if(it.next().equals("love"))
it.add("Oracle");

}