Java导致ConcurrentModificationException所有原因

 更新时间:2026年01月20日 10:49:16   作者:猩火燎猿  
ConcurrentModificationException是Java集合框架抛出的运行时异常,表示集合在遍历过程中被结构性修改了,导致迭代器无法保证一致性,下面就来介绍一下几种原因,感兴趣的可以了解一下

1. 什么是 ConcurrentModificationException?

它是 Java 集合框架抛出的运行时异常,表示集合在遍历过程中被结构性修改了,导致迭代器无法保证一致性。

2. 异常触发的底层机制

Java 集合(如 ArrayList、HashSet 等)在创建迭代器时,会记录集合的结构性修改次数(modCount)。每次集合结构发生变化(如 add、remove),modCount 增加。迭代器内部有一个 expectedModCount,每次调用 next()/hasNext() 时,会检查 modCount 是否和 expectedModCount 一致。如果不一致,就抛出 ConcurrentModificationException。

3. 导致 ConcurrentModificationException 的所有常见原因

3.1 遍历过程中直接修改集合

错误代码示例:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s); // 错误!遍历时直接修改集合
    }
}

原因: for-each 底层用的是 iterator,直接用 list.remove() 修改集合,modCount 改变,expectedModCount 没变,抛异常。

3.2 用 Iterator 遍历时,直接用集合的 add/remove 方法修改集合

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        list.remove(s); // 错误!应使用 it.remove()
    }
}

3.3 多线程并发修改集合

一个线程遍历集合,另一个线程同时修改集合(结构性操作),也会抛异常。

示例:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
new Thread(() -> {
    for (Integer i : list) {
        System.out.println(i);
    }
}).start();
 
new Thread(() -> {
    list.add(4); // 并发修改
}).start();

3.4 在 for-each 循环中调用集合的 remove/add

for (String s : list) {
    list.remove(s); // 错误
}

正确做法: 用 Iterator 的 remove 方法。

3.5 对 Map 的 keySet/values/entrySet 进行遍历时修改 Map

Map<String, Integer> map = new HashMap<>();
map.put("a", 1); map.put("b", 2);
for (String key : map.keySet()) {
    map.remove(key); // 错误
}

3.6 迭代器遍历时,集合结构被外部方法修改

如果在遍历过程中调用了会修改集合结构的方法(即使不是直接在循环体里),也会导致异常。

3.7 使用 fail-fast 集合(如 ArrayList、HashSet、HashMap)遍历时结构性修改

Java 的大多数集合都是 fail-fast 的(快速失败),即检测到并发修改就立即抛异常。

4. 什么是结构性修改?

结构性修改指的是影响集合元素数量或排列的操作,比如 add、remove、clear、put(Map),而仅仅修改元素内容(如 set(index, value))不算结构性修改。

5. 如何避免 ConcurrentModificationException?

用 Iterator 的 remove 方法删除元素

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        it.remove();
    }
}

用并发安全集合
如 CopyOnWriteArrayListConcurrentHashMap,这些集合不会抛 ConcurrentModificationException。

遍历前收集要删除的元素,遍历后统一删除

List<String> toRemove = new ArrayList<>();
for (String s : list) {
    if (条件) toRemove.add(s);
}
list.removeAll(toRemove);

使用 ListIterator 的 add/remove/set 方法

6. 其他补充

  • ConcurrentModificationException 只是 fail-fast 机制的一部分,不能保证百分百检测到所有并发修改。
  • 在多线程环境下,优先使用并发集合或加锁处理。

7. 总结

所有原因本质:
遍历过程中集合结构被直接或间接修改,导致迭代器检测到不一致。

常见场景:

  • for-each 循环中直接 add/remove
  • Iterator 遍历时集合 add/remove
  • 多线程并发修改
  • Map 的 keySet/values/entrySet 迭代时修改 Map

避免方式:

  • 用 iterator.remove()
  • 用并发集合
  • 遍历前收集、后批量修改

8. 底层原理再深入

8.1 modCount 和 expectedModCount

  • 每个 fail-fast 集合(如 ArrayList、HashMap)内部有一个 modCount 字段,代表结构性修改次数。
  • 创建迭代器时,迭代器保存一份 expectedModCount
  • 每次迭代器操作(如 next()remove())时,都会检查 modCount 是否和 expectedModCount 一致。
  • 如果不一致,说明集合被外部修改,抛出 ConcurrentModificationException

源码片段(以 ArrayList 为例):

public E next() {
    checkForComodification(); // 检查
    ...
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

9. 特殊集合的处理

9.1 并发集合不会抛出该异常

  • CopyOnWriteArrayListConcurrentHashMap 等并发集合,内部机制不同,不会 fail-fast。
  • 例如,CopyOnWriteArrayList 每次修改都会复制一份新数据,迭代器遍历的是旧快照,不关心后续修改。

9.2 老的 Vector、Hashtable

  • 这些集合的方法都加了同步锁(synchronized),不会抛出 ConcurrentModificationException,但性能较低。

10. 实际项目中的规避策略

10.1 单线程环境

  • 删除元素用 iterator.remove()。
  • 遍历前收集需要删除的元素,遍历后统一删除。
  • 不要在 for-each 或普通 for 循环中直接 remove/add。

10.2 多线程环境

  • 使用并发集合(如 CopyOnWriteArrayList、ConcurrentHashMap)。
  • 使用同步块(synchronized)保护遍历和修改操作。
  • 分批处理:先收集需要修改的数据,后统一操作。

10.3 遍历 Map 的安全删除

  • 用 Iterator<Map.Entry<K,V>> 遍历,然后用 iterator.remove() 删除当前 entry。
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    if (entry.getValue() < 10) {
        it.remove();
    }
}

11. 排查和调试方法

11.1 查看异常堆栈

  • 异常堆栈会指向集合的迭代器方法(如 next()),分析调用链,定位哪里修改了集合。

11.2 检查所有集合修改点

  • 搜索代码中所有对集合的结构性操作(add、remove、clear等),看是否在遍历期间被调用。

11.3 多线程场景

  • 检查是否有线程并发修改集合,必要时加锁或用并发集合。

12. 面试延伸问题

  1. 什么是 fail-fast?什么是 fail-safe?举例说明。
    • fail-fast:检测到并发修改立即抛异常(如 ArrayList)。
    • fail-safe:迭代器遍历的是快照,不抛异常(如 CopyOnWriteArrayList)。
  2. 如何安全地在遍历过程中删除集合元素?
    • 用 iterator.remove()。
  3. ConcurrentModificationException 一定能检测到所有并发修改吗?
    • 不能,只能检测到部分典型场景。
  4. 为什么并发集合不会抛 ConcurrentModificationException?
    • 并发集合设计了特殊机制,如快照、分段锁等,保证遍历安全。

13. 真实案例分析

案例:批量删除数据库记录时同步维护缓存集合

假设你有一个缓存 List,批量删除数据库记录后也要同步删除 List 中的元素:

// 错误做法,可能抛异常
for (User user : cacheList) {
    if (shouldDelete(user)) {
        cacheList.remove(user);
    }
}
 
// 正确做法
Iterator<User> it = cacheList.iterator();
while (it.hasNext()) {
    User user = it.next();
    if (shouldDelete(user)) {
        it.remove();
    }
}

14. 代码示例:多线程并发修改

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) list.add(i);
 
Thread t1 = new Thread(() -> {
    for (int i : list) {
        // 遍历
    }
});
Thread t2 = new Thread(() -> {
    list.remove(50); // 并发修改
});
t1.start(); t2.start();
// 可能抛 ConcurrentModificationException

15. 总结

  • ConcurrentModificationException 是集合 fail-fast 机制的体现。
  • 本质原因是遍历期间结构性修改集合。
  • 规避方法:用 iterator.remove(),用并发集合,多线程加锁,遍历后统一修改。
  • 多线程场景优先用并发集合,普通集合加锁也可。
  • 面试常问底层原理、fail-fast 与 fail-safe、实际场景规避。

到此这篇关于Java导致ConcurrentModificationException所有原因的文章就介绍到这了,更多相关Java导致ConcurrentModificationException原因内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解SpringMVC在IDEA中的第一个程序

    详解SpringMVC在IDEA中的第一个程序

    Spring MVC 属于Spring Framework的一部分,是一种Spring框架内置的MVC的实现。这篇文章主要介绍了SpringMVC在IDEA中的第一个程序,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • OutOfMemoryError内存不足和StackOverflowError堆栈溢出示例详解

    OutOfMemoryError内存不足和StackOverflowError堆栈溢出示例详解

    这篇文章主要为大家介绍了OutOfMemoryError内存不足和StackOverflowError堆栈溢出示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • SpringBoot 实现流控的操作方法

    SpringBoot 实现流控的操作方法

    本文介绍了限流算法的基本概念和常见的限流算法,包括计数器算法、漏桶算法和令牌桶算法,还介绍了如何在Spring Boot中使用Guava库和自定义注解以及AOP实现接口限流功能,感兴趣的朋友一起看看吧
    2024-12-12
  • 解决Java原生压缩组件不支持中文文件名乱码的问题

    解决Java原生压缩组件不支持中文文件名乱码的问题

    本篇文章主要介绍了解决Java原生压缩组件不支持中文文件名乱码的问题,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2017-03-03
  • Java中的CopyOnWriteArrayList你了解吗

    Java中的CopyOnWriteArrayList你了解吗

    CopyOnWriteArrayList是Java集合框架中的一种线程安全的List实现,这篇文章主要来和大家聊聊CopyOnWriteArrayList的简单使用,需要的可以参考一下
    2023-06-06
  • JAVA“无法验证证书。将不执行该应用程序。”提示解决办法

    JAVA“无法验证证书。将不执行该应用程序。”提示解决办法

    这篇文章主要给大家介绍了关于JAVA“无法验证证书,将不执行该应用程序”提示的解决办法,要解决Java无法验证证书的问题,可以尝试下本文的方法,需要的朋友可以参考下
    2024-03-03
  • Java递归以及根据节点取子集合方式

    Java递归以及根据节点取子集合方式

    文章介绍了Java中递归的使用方法,包括如何根据节点构建树形结构以及如何反向递归获取所有子节点,提供了递归方法的参数解释和示例代码,希望对大家有所帮助
    2024-12-12
  • SpringBoot使用Graylog日志收集的实现示例

    SpringBoot使用Graylog日志收集的实现示例

    Graylog是一个生产级别的日志收集系统,集成Mongo和Elasticsearch进行日志收集,这篇文章主要介绍了SpringBoot使用Graylog日志收集的实现示例,感兴趣的小伙伴们可以参考一下
    2019-04-04
  • idea修改只读/可写状态全过程

    idea修改只读/可写状态全过程

    本文记录了解决IntelliJ IDEA(打开文件只读问题的过程,在设置中找到Editor下的Reader Mode,取消勾选选第一个可选框即可解决问题,同时提到该选项看起来很好看,本文仅供参考,希望能对读者有所帮助
    2026-05-05
  • Spring超详细讲解创建BeanDefinition流程

    Spring超详细讲解创建BeanDefinition流程

    Spring在初始化过程中,将xml中定义的对象解析到了BeanDefinition对象中,我们有必要了解一下BeanDefinition的内部结构,有助于我们理解Spring的初始化流程
    2022-06-06

最新评论