关于操作集合的时候报java.util.ConcurrentModificationException异常分析(并发修改异常)

本文分析了在操作集合时遇到的`ConcurrentModificationException`异常,通过复现异常、追踪异常信息及源码分析,揭示了异常产生的原因——在迭代过程中调用了集合的修改方法。解决方案是在迭代时使用迭代器的`remove()`方法而不是集合的`remove()`。文章还探讨了`Iterator`的获取以及`for-each`循环与迭代器的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集合可以说是平时开发用到最多的,今天就记录一下操作集合的时候报了一个ConcurrentModificationException异常,就这个问题来分析原因,避免后面同学入坑,以下是本文目录大纲:
1.复现异常出现的情景 (jdk 1.8 )
2.跟踪异常信息追踪报错逻辑
3.源码分析以及解决方案

若有不正,多多见谅
若有雷同,算我抄你
欢迎批评指正 ,转载请标明原文链接

复现ConcurrentModificationException异常的出现
public class ExceptionTest {
    public static void main(String[] args) {
        ArrayList<Object> list = Lists.newArrayList();
        list.add("1");
        Iterator<Object> iterator = list.iterator();
        while(iterator.hasNext()){
            Object s = iterator.next();
            //System.out.println(s);
            if(s.equals("1")){
                list.remove(s);
            }
        }

    }
}

运行结果:
在这里插入图片描述
ConcurrentModificationException ,内心震惊!这个应该没问题吧,又运行一遍,还是这

根据异常信息追踪报错逻辑

还是老规矩(一定要看代码中的注释,把排查思路搞清楚,然后再自己走一遍),看异常信息从下往上看着走,直接定位到ArrayList$Itr.next(ArrayList.java:857)点击进去看代码如下:找到next方法的调用处,

public Iterator<E> iterator() {
        return new Itr();
    }
   
private class Itr implements Iterator<E> {
        // 下一个返回元素的索引,也就是游标,用来判断是否还有元素
        int cursor;       
        // 返回最后一个元素的索引,如果没有元素了,就返回-1
        int lastRet = -1; 
        //这个是关键的点,把modCount值给到expectedModCount
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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];
        }

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

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
        // 原来报错的根源在这个地方啊,但是这两个值不想等是怎么来的呢?继续追溯点进去看下
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

那就看一下这两个变量到底是什么?首先看看modCount,原来是抽象类里面的一个成员变量,上面的注释一定要回头好好看看,很重要,意思在此先不翻译,逻辑优先

// 这个值在ArrayList中执行add方法的时候,会自增
protected transient int modCount = 0;

再去找expectedModCount ,点进去之后来到了ArrayList的里面,也就是上面那个代码的这里 int expectedModCount = modCount;
所以到这里我们就清楚了报错的原因,那么这两个值怎么不相等呢,我怎么判断呢,把最初的那个注释代码去掉,看结果如下:
在这里插入图片描述
结果打印出来了,那就是说在第二次循环的时候,才报错的,这之间我只是做了一个删除操作,所以就是这个删除操作引发的血案,聚焦到这里,看删除是怎么操作的,继续看源码:

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        // 代码走到这里,
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                // 调用这个方法删除
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    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
    }

删除之前modCount++了,这里我们需要注意一下,列出几个值

第一次循环之前:
cursor = 0;
lastRet = -1;
modCount = 1;(因为list有一个add操作);
expectedModCount = modCount = 1;
size = 1;
第一次循环结束之后,也就是删除执行完:
cursor=1;
lastRet =0;
modCount =2;
expectedModCount =1;
size = 0;

所以第二次调用iterator.hasNext()的时候,判断cursor != size 肯定为true,
接着往下走再去调用Iterator.next()的时候,实际就是调用checkForComodification();这个方法,再次比较两个值的,结果肯定不相等啦,所以就抛出java.util.ConcurrentModificationException啦!

这里呢,就多追溯一点,这个Iterator,有的同学好像不是很清楚,我们为什么从集合里面可以直接拿到迭代器呢?就好比上面的那个list.iterator(),集合里面我们知道list和set都是接口Collection的实现类,我们去看Collection这个接口就知道它是继承Iterable这个接口的,所以拿到迭代器就明了了。

分析完报错原因就清晰明了怎么解决了,根本就问题在于,调用list.remove()方法导致modCount和expectedModCount的值不一致。
注意啦,像使用for-each进行迭代实际上也会出现这种问题在调用删除的方法的时候,for-each再针对集合的循环实际底层就是迭代器,针对数组是另外一种情况,这个本文暂不详细诉说

解决方案

删除操作不用list的remove,而是用迭代器中的remove方法

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

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

其实就是多了一步赋值操作expectedModCount = modCount
来重新跑下,看下结果

public class ExceptionTest {
    public static void main(String[] args) {
        ArrayList<Object> list = Lists.newArrayList();
        list.add("1");
        Iterator<Object> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(list.size());
            Object s = iterator.next();
           System.out.println(s);
            if(s.equals("1")){
//                list.remove(s);
                iterator.remove(); //就是这行代码
            }
        }

    }
}

运行结果:
在这里插入图片描述
完全正确!本文讲述的都是单线程情况下,多线程情况下,还没有遇到过,也没有测试过,不过网上应该有很多的文章参考。

思考:

试想一下如果代码换成这样去执行,结果是怎么样呢?自己动手试一下吧

public class ExceptionTest {
    public static void main(String[] args) {
        ArrayList<Object> list = Lists.newArrayList();
        list.add("1");
        list.add("2");
        Iterator<Object> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(list.size());
            Object s = iterator.next();
           System.out.println(s);
            if(s.equals("1")){
                list.remove(s);
//                iterator.remove();
            }
        }

    }
}

欢迎在评论区分享你的研究成果,一起学习进步喽!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值