38

聊聊 ConcurrentModificationException [ 黄小豆的博客 ]

 4 years ago
source link: https://jacobchang.cn/ConcurrentModificationException.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

聊聊 ConcurrentModificationException

2019-06-03 /

今天有朋友突然在群里抛出一句,”java中使用foreach遍历时,为啥不让删除元素呢?设计ConcurrentModificationException的意义是什么目的呢?如果单线程操作,还需要吗?” 。今天我们就来聊一聊这件事。

如果在使用 Iterator 遍历一个元素的时候,如果同时使用 List.remove() 方法去移除元素,会报出 ConcurrentModificationException 异常。如何避免发生这种异常,以及如何为什么会抛出这个异常。

先上代码:

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}

for (Integer integer : list) {
if (integer == 7) {
list.remove(integer);
}
}
}

异常日志:

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 collections.Main2.main(Main2.java:16)

我们看上面的代码貌似没有使用 Iterator 呀,其实上面的 foreach 循环就是其实就是使用了这个对象,甚至如果你使用这样的语句 System.out.print(list) 也会使用的list对象的迭代器。我们把上面的代码改写一下

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next(); // 此处会抛出异常
if (next == 7) {
list.remove(next);
}
}
}

iterator.next() 究竟在哪里抛出了一场,我们来一探究竟。首先 iteratorArrayList.Itr 类型,next 方法源码如下

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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); // 此处抛出异常
}

到这里我们明白,抛出异常的原因是因为 modCount != expectedModCount,但是这两个字段是什么含义呐,为什么会不相等?

modCountAbstractList的一个字段,用来表示这个容器实例被修改的次数,如果容器中的元素有增加、移除、替换等操作的都会修改这个值。expectedModCountArrayList.Itr 类的一个字段。在创建迭代器的时候,会将modCount 赋值给expectedModCount

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; // 赋值

Itr() {}

然后我们来结合我们的代码来分析,在循环中我们移除了元素值为 7 的元素 list.remove(next); 在该方法内部调用了 fastRemove 方法

/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++; // 修改了 modCount 的值
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
}

等到到达下一次 Integer next = iterator.next(); 的时候就触发了 ConcurrentModificationException异常。

以上,对于异常的原因分析就结束了。

如何避免这个异常

改写上面的代码,利用 iterator 来移除元素即可。

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next == 7) {
iterator.remove(); // 改写移除元素的方法
}
}
}

我们来看看 iterator.remove();发生了什么

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet); // 移除了元素
cursor = lastRet; // 把当前游标回拨
lastRet = -1; // 把上一个返回的元素的游标重置
expectedModCount = modCount; // 重新吧 modCount 的值赋给 expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

单线程的影响

看到这里你可能会问,如此大费周章的去删除一个元素究竟是为了什么?

通过两种移除元素方法的对比可以发现,使用iterator.remove();移除元素,仅仅支持移除当前迭代的元素,并且在不进入下一次迭代前iterator.remove();只能调用一次。而通过 list.remove 的方式可以移除任意的元素。通过使用迭代器移除可以获得一个容器确定的视图。下面我们假装list.remove不会抛出异常,来举个例子

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
for (int i = 0; i < 7; i++) {
iterator.next();
}

for (Integer integer : list) {
if (integer == 1) {
list.remove(integer);
break;
}
}

System.out.println(iterator.next()); // expected 7, but found 8
}

我们本来是想拿到一个完整的迭代,但是却缺少了 7 这个元素。这里仅仅是做了打印处理,如果是要利用迭代器计算这个容器所有元素的和,那么这个和必然是不符合预期的。有人会说我自己知道移除了 1 这个元素,并且所以我预期的和就是没有 7 的和。如果真的是这样的话,这样的代码维护起来将是一个噩梦,你要注意在哪里移除了某个元素,以及是否会对后面的逻辑产生影响。当代码逻辑进一步复杂的时候,这样的做法会让代码表现更加的不可预期。

多线程的影响

如果是在多线程中使用 ArrayList,仅针对本文中涉及的元素来看,modCount的可见性会有问题,每个线程看到的modCount的大小可能是不一样的,同时modCount++等改变值得操作也会没有同步性措施而变得失去原子性。

Vector作为同步版本的 List的做法是在有关 modCount 操作的地方使用 synchronized来保证可见性和同步。

由于 list.iterator(); 每次都会生成新的迭代器,所以 cursorlastRet变量是线程封闭的,无需同步。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK