You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

什么原因会导致ConcurrentModificationException异常?

聊透ConcurrentModificationException:结合ArrayList迭代器源码拆解

嘿,这个问题问得特别实在!ConcurrentModificationException(咱们简称CME)是Java集合里最容易踩的坑之一,刚好你贴的这段就是ArrayList内部迭代器Itr的核心代码,咱们一步步拆解清楚。

一、为什么会触发ConcurrentModificationException?

核心原因就俩变量:modCount和expectedModCount的不一致。

  • modCount是ArrayList类内部的成员变量,用来记录集合被修改的总次数——只要你调用add、remove、clear这类会改变集合结构的方法,它就会自增1。
  • expectedModCount是迭代器Itr的成员变量,在你调用list.iterator()创建迭代器的时候,它会直接复制当前ArrayList的modCount值。

而你代码里的checkForComodification()方法,就是迭代器的"守门人":

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

每次调用迭代器的next()、remove()方法前,都会先执行这个检查——只要发现modCount和expectedModCount不一样,直接抛出CME,告诉你"集合被偷偷修改了!"

常见触发场景有两种:

  • 单线程踩坑:用迭代器遍历集合的同时,直接调用ArrayList自己的add/remove方法(不是迭代器的remove)。比如:
    ArrayList<String> list = new ArrayList<>(List.of("a", "b", "c"));
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        String item = it.next();
        if (item.equals("b")) {
            list.remove(item); // 直接调用list的remove,modCount+1,但expectedModCount没更新
        }
    }
    
    这时候下一次调用it.next()时,检查就会失败,抛出CME。
  • 多线程并发:一个线程用迭代器遍历,另一个线程修改集合。因为modCount不是线程安全的,两个线程操作后很容易出现modCount和expectedModCount不一致的情况。

二、你贴的这段迭代器代码逻辑合理吗?

太合理了!这是Java集合"快速失败(fail-fast)"机制的典型实现,每一步都在守护迭代的安全性:

1. hasNext()方法

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

逻辑极简:cursor是迭代器当前指向的下一个元素的索引,只要它不等于集合的size,就说明还有元素可以遍历。完全符合直觉,没有多余操作。

2. next()方法

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification(); // 先检查,确保集合没被外部修改
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException(); // 没元素了还调用next?抛异常
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException(); // 防御性检查:正常情况下cursor不会超过elementData长度,除非集合被外部非法修改
    cursor = i + 1; // 移动cursor到下一个位置
    return (E) elementData[lastRet = i]; // 记录lastRet(最后返回的元素索引),然后返回元素
}

这里的每一步都有意义:

  • 先做检查,把非法修改扼杀在摇篮里;
  • 两次越界判断,分别处理"遍历完了还调用next"和"集合被外部非法修改导致结构混乱"的情况;
  • 移动cursor并记录lastRet,为后续的remove()操作做准备。

3. remove()方法

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException(); // 还没调用next就想remove?不行
    checkForComodification(); // 先检查,确保调用remove前集合没被外部修改
    try {
        ArrayList.this.remove(lastRet); // 调用ArrayList的remove方法删除元素
        cursor = lastRet; // 因为删除元素后,后面的元素前移了,cursor要回退一位,避免跳过下一个元素
        lastRet = -1; // 标记lastRet无效,防止重复调用remove
        expectedModCount = modCount; // 关键!同步expectedModCount和modCount,因为这次是迭代器自己发起的修改,合法,所以不会触发后续检查失败
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException(); // 把索引异常包装成CME,统一异常类型
    }
}

这是迭代器最巧妙的地方:只有通过迭代器自己的remove()方法修改集合,才会同步expectedModCount,所以不会触发CME。这也是为什么我们遍历集合要删除元素时,必须用迭代器的remove()而不是集合自己的remove()。

4. checkForComodification()方法

这个方法就是快速失败机制的核心:它不允许在迭代过程中,集合被迭代器之外的操作修改。一旦发现,立刻抛出异常,避免程序继续运行出现更隐蔽的错误(比如遍历到错误的元素、数组越界、数据不一致等)。

总结

这段迭代器代码的设计非常严谨,完全围绕"快速失败"原则:在不安全的集合修改操作发生时,尽早抛出异常,提醒开发者修正代码,而不是让程序带着错误继续运行。这也是Java集合框架保证迭代安全性的重要手段。

内容的提问来源于stack exchange,提问作者Ankush Dutt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:36:27