Java中ConcurrentModificationException异常处理

ConcurrentModificationException是单线程下“边迭代边修改”触发的fail-fast机制,非多线程并发导致;底层通过modCount与expectedModCount比对检测结构变更,不一致则抛异常。

为什么遍历集合时修改会抛出ConcurrentModificationException

这个异常不是因为多线程并发导致的(虽然名字容易误导),而是ArrayListHashMap等非线程安全集合在单线程中「边迭代边修改」触发了快速失败(fail-fast)机制。底层靠modCount计数器检测结构变更:每次调用add()remove()等方法都会递增它,而迭代器在next()hasNext()时会校验当前modCount是否与初始化时一致,不一致就直接抛异常。

常见错误现象:

  • for-each循环遍历ArrayList,内部调用iterator(),然后在循环体里调用list.remove(obj)
  • Iterator遍历,但调用的是集合自身的remove()而非iterator.remove()
  • 多个线程共用同一个ArrayList,一个在读(遍历),一个在写(增删

单线程下安全删除元素的正确方式

核心原则:必须通过迭代器自身提供的remove()方法来删除当前元素,它会同步更新expectedModCount,避免校验失败。

List list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("b".equals(s)) {
        it.remove(); // ✅ 正确:调用迭代器的 remove()
    }
}

其他可行方案:

  • 收集待删元素,遍历结束后统一调用list.removeAll(toRemove)
  • 使用removeIf()(Java 8+):list.removeIf(s -> "b".equals(s))
  • 倒序索引遍历(仅适用于ArrayList等支持随机访问的集合):for (int i = list.size() - 1; i >= 0; i--) { if (needRemove(list.get(i))) list.remove(i); }

多线程环境下的替代集合选择

如果确实需要多线程并发读写且遍历时不报错,不能靠加锁硬扛(锁住整个遍历过程性能差、易死锁),应换用线程安全的集合实现:

  • CopyOnWriteArrayList:适合读多写少场景;遍历时操作的是快照,写操作会复制整个数组,Iterator不支持remove()
  • ConcurrentHashMap:迭代其keySet()values()entrySet()不会抛ConcurrentModificationException,但结果可能不反映实时状态(弱一致性)
  • BlockingQueue子类(如LinkedBlockingQueue):适用于生产者-消费者模型,提供线程安全的增删操作

注意:VectorHashtable虽是线程安全的,但它们的迭代器仍是fail-fast的,同样会抛该异常。

调试时如何快速定位问题源头

异常堆栈通常只显示在next()hasNext()处抛出,但真正引发modCount变化的位置往往在别处。关键看异常信息中的「at」行和上层调用链:

  • 检查堆栈中是否有remove()add()clear()等结构修改方法出现在迭代逻辑附近
  • 确认是否在Lambda表达式、Stream操作(如forEach)中隐式调用了集合修改方法
  • 留意匿名内部类或监听器回调里是否意外修改了外部集合

最容易被忽略的一点:即使你没显式写remove(),某些工具方法(比如Collections.synchronizedList()包装后返回的代理对象,其iterator()仍不支持并发修改)也会导致相同行为——它只是同步了单个方法,没解决迭代器一致性问题。