《阿里巴巴 Java 开发手册》第一章里的第五节的第七点是这么说的:

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

里面举了这样一个反例:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (String item : list) {
if (“1”.equals(item)) {
list.remove(item);
}
}

复制代码

其实 Java 的forEach 写法内部就是迭代器,大家可以把上面的代码理解为以下代码:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (“1”.equals(item)) {
list.remove(item);
}
}

复制代码

有了这一层理解后,那我们以 ArrayList 为例,看看其内部的iterator方法:

public Iterator<E> iterator() {
	return listIterator();
}

public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ListIterator&lt;E&gt;() {
          hasNext()...
          next()...
        ...
					}

}

复制代码

由于listIterator()方法内的内部类ListIterator的代码太多,我就不一一贴出来了,因为我们重点只看两个方法:hasNext()next(),接下来我会通过断点调试让大家明白为什ConcurrentModificationException是偶尔出现:

断点调试

设置断点

我在这三处地方都打了断点,这样我们就能大概清楚整个流程:

image.png

image.png

运行调试

P1

好的,我们看到已经定位到第一个断点位置了,从 idea 提供的信息我们也可以看出 list 的大小为 2:

image.png

接着往下走,又来到了第二个断点的位置,在上面我已经说了forEach的语法的原理了,所以这样会走到haxNext()函数这里,这里的cursor是指当前迭代器的指针,而size是当前集合的大小:

image.png

继续走,我们会来到第三个断点:

image.png

圈红 1

注意我圈红的第一处地方,我们进入 checkForComodification 里:

final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}
复制代码

可以看到,这就是我们报错的关键点,这里的modCount变量是指集合被操作的次数,比如像add()remove()这些方法都会让modCount + 1,而expectedModCount是指集合的一个预期操作次数,在部分操作里会被重置为modCount,比如add()方法里。

因为我们上面添加了两个元素,所以modCountmodCount都是 2。

圈红 2

接着我们看第二处圈红的地方,我们可以发现,每一次next()的时候指针都会移动,这很好理解。

P2

断点继续,因为第一个元素就是 1,所以这里匹配上了:

image.png

我们进入到remove()方法里面,因为我们是按照对象删除的,所以会进入第二个分支:

image.png

接着我们再进入fastRemove()方法,可以看到modCount + 1了:

image.png

P3

继续往下走,我们又回到最开始的地方,但仔细点你会发现 list 的大小从 2 变成 1 了:

image.png

然后我们又来到了hasNext()这里了,因为cursorsize都是 1,所以循环就终止了:

image.png

image.png

吃鲸

这里你是不是懵逼了,咦?说好的报错呢?怎么没报错了?

咳咳,其实是因为有时候会出现像上面这种巧合的情况,就是在hasNext()方法校验的时候,cursor刚好不等于size,然后就退出了,而刚好集合又遍历完了,but,这个情况是很少出现的,一般都会抛出ConcurrentModificationException异常,所以大家不要有侥幸的心理。

还原报错

下面我们还是以上面的例子,只是这次我把删除的对象从 1 改为 2

image.png

运行调试后跟上面的 P1 和 P2 是一样的,所以这里我就不重复了,唯一不同的地方在 P3。这里我们已经来到第二次循环,校验元素后会删除元素 2:

image.png

在第三次循环,(这里是指第三次进行hasNext())的时候,我们可以看到 list 的大小是 1 了:

image.png

ok,我们继续往下,这里大家要特别注意,可以看到cursor此时是 2,而size却是 1,所以循环还可以继续

image.png

前面我们说过next()方法里的checkForComodification()是检查操作次数的,所以这里就不复述了:

image.png

我们进入到checkForComodification()里,可以看到modCount是 3(因为remove()操作**modCount**自增了),而expectedModCount是 2,所以就报错了

image.png

  • Java

    Java,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台的总称。用 Java 实现的 HotJava 浏览器(支持 Java applet)显示了 Java 的魅力:跨平台、动态的…

    380 引用 • 6 回帖
感谢    赞同    分享    收藏    关注    反对    举报    ...