集合的并发修改异常


摘要

有关 集合的并发修改异常 两种场景的剖析

情景一:多线程同时迭代共享集合并修改共享集合#

/**
构建一个随机数List
开启多个线程,同时执行共享List迭代器,并删除List中符合条件的元素
*/
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
    arrayList.add(new Random().nextInt(100_000_000));
}
for (int i = 0; i < 20; i++) {
    new Thread(()->{
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
            if(next > 1_000_000){
                iterator.remove();
            }
        }
    }).start();
}

执行结果:#

结果会抛出一推异常,比如

Exception in thread "Thread-0" java.util.ConcurrentModificationException...

原因分析:#

为什么在集合的迭代器中修改元素会抛出 ”并发修改异常”?

首先,了解一个 AbstractList 的成员变量:

protected transient int modCount = 0;

再看看 ArrayList 的迭代器中的一个成员变量:

private class Itr implements Iterator<E> {
    //...
    int expectedModCount = modCount;
    //..

我没每次调用迭代器 next() \ remove() ..等一些方法时,这些方法内部都会调用一个检查方法 checkForComodification():

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

从这里可以看出 expectedModCount 和 modCount 的关系,一个集合可以实例化出多个迭代器,但一个集合只能有一份 modCount 成员变量, 也就是说 多个迭代器共享一个 modCount。

modCoune 和 expectedModCount 要保持一致,否者就会抛出 并发修改异常。

那又是什么情况导致 modCoune 发生变化呢?

案例中,我们是多个线程同时迭代并删除符合条件的元素,那么具体看看 ArrayList 中迭代器的 remove()

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();
    }
}

发现迭代器的 remove() 本质上还是调用了集合本身的 remove():

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;
}

发现,里面修改了 modCount,每次调用 remove(),modCount 就会自增1。

从而得知,当有多个线程执行迭代器来删除元素时,就会导致 modCount 的混乱,从而发生并发修改异常。

另外一种可能的异常

我们重点关注迭代器的 remove() 其中另外一种发生并发修改异常的情况:

public void remove() {
    // ...
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
        // !!!
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

可以看出,try 代码快可能会发生 IndexOutOfBoundsException

就是, 而导致的 并发修改异常, 那又是什么情况导致 IndexOutOfBoundsException 呢?

还是多线成引起的问题,这很好解释,假如一个集合 list 有5个元素,有两个线程同时遍历迭代器操作集合,A线程再遍历到第3个元素时,发现符合条件并删除这个元素,迭代器是根据 size来遍历集合的,A线程改变了集合的 size,可是B线程却不知道 size 已经被改变,结果总会发生 索引越界异常

解决方案:#

ArrayList、LinkedList、SetMap、HashMap,它们本身是安全的,但它们并不支持多线程,

尝试考虑使用相关支持多线程的集合类型!

情景二:单线程执行增强for循环中修改集合元素#

ArrayList<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "tom", "kobe", "jordan", "tracy", "westbook");
for(String s : arrayList){
    if("jordan".equals(s)){
        // arrayList.remove(s);
        arrayList.add(s);
    }
}

执行结果:#

Exception in thread "main" java.util.ConcurrentModificationException

原因分析:#

可以看到,在单线程里,通过增强 for 循环来修改集合元素,还是会抛出并发修改异常,这是为啥?

首先,我们用集合本身的修改元素方法,就会导致 modCount 的增加,

我们有没有用迭代器的方法来修改集合元素,就会导致 expectedModCount 与 modCount 不能得到同步,

但是!增强 for 循环遍历集合,本质上就是用集合的迭代器来遍历集合,其中,一定用到了 hasNext() \ next(), 而它们中都调用了 checkForComodification() 来判断 expectedModCount 与 modCount,从而导致 ConcurrentModificationException,

解决方案#

不管是单线程还是多线程场景,使用增强 for 循环遍历集合,最多读取集合的元素,不要试图去修改集合的元素,从而避免异常的发生

https://www.cnblogs.com/ghnb1/p/17388554.html

@脚注



  1. 索引越界异常 ↩︎

  2. 集合的长度 ↩︎

  3. 并发修改异常 ↩︎

作者:Hong•Guo

出处:https://www.cnblogs.com/ghnb1/p/17388554.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Hong•Guo  阅读(123)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示