转载

聊聊 ConcurrentModificationException

今天有朋友突然在群里抛出一句,”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 变量是线程封闭的,无需同步。

原文  https://jacobchang.cn/ConcurrentModificationException.html
正文到此结束
Loading...