关于集合中的并发修改异常及处理方式

 更新时间:2025年06月30日 10:09:36   作者:找不到、了  
这篇文章主要介绍了关于集合中的并发修改异常及处理方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

前言

关于集合的总结,可参考如下图:

在Java中,像ArrayList这样的集合类使用迭代器的时候,如果在遍历过程中直接修改集合(比如remove),可能会导致ConcurrentModificationException。

为什么是可能会产生导致ConcurrentModificationException?

因为在修改集合的结构化修改modeCount的过程中,如果修改了集合的索引(索引前移),则不会发生异常。

1、并发异常介绍

ConcurrentModificationException的核心原因是:迭代器检测到集合在遍历过程中被修改,导致状态不一致

1.1、for-each的本质

在 Java 中,如果你在for-each循环(即增强型 for 循环)中直接对集合执行remove()操作,会抛出ConcurrentModificationException。

这是因为for-each循环底层依赖于迭代器(Iterator)来遍历集合,而迭代器在设计时为了保证遍历的一致性和安全性,对集合的结构修改有严格的限制。

如下所示,for-each的本质:

for (Element e : collection) {
    // do something
}

等价于:
Iterator<Element> it = collection.iterator();
while (it.hasNext()) {
    Element e = it.next();
    // do something
}

1.2、调用list.remove()的后果

在循环中直接调用list.remove()(element)或list.remove()(index),会直接修改集合的结构,导致modCount自动递增。

此时迭代器的expectedModCount并未更新,因此在下一次调用it.next()时,会检测到不一致并抛出异常。

2、迭代器

2.1、设计原理

Java 集合框架中的迭代器(Iterator)在设计时引入了一种并发修改检查机制,用于在遍历过程中检测集合的结构是否被外部修改。

机制的核心目的保证迭代过程中集合的一致性,防止因并发修改导致的不可预期行为。

1、快速失败(Fail-Fast)策略

Java 集合的迭代器采用快速失败策略:一旦检测到并发修改,立即抛出异常,防止后续操作产生不可预知的结果。快速失败不能保证 100% 检测到所有并发修改,但能显著降低错误概率。

2、安全性与性能的权衡

并发检查机制增加了少量性能开销,但保障了迭代过程的安全性。对于高并发场景,可选择线程安全的集合类(如ConcurrentHashMap)。

2.2、并发检查机制的属性

1.modCountexpectedModCount

modCount:

  • 是集合类(如ArrayList、HashMap)中的一个字段,表示集合的结构性修改次数(如添加、删除元素)。
  • 每次对集合进行结构性修改(如add()、remove()),modCount会自动递增。

expectedModCount:

是迭代器内部保存的一个字段,表示迭代器创建时集合的modCount值。

  • 在调用it.next()时,迭代器内部会检查当前集合的modCount是否与迭代器创建时记录的expectedModCount一致
  • 如果不一致,就会抛出ConcurrentModificationException。

2.检查逻辑

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

如果集合的modCount被修改(即modCount != expectedModCount),迭代器会抛出ConcurrentModificationException,表示检测到并发修改。

2.3、机制的工作流程

1、迭代器创建时

迭代器在创建时会记录当前集合的modCount值,并将其赋值给expectedModCount

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int expectedModCount = modCount; // 记录初始状态
    ...
}

2、迭代过程中调用next()

每次调用next()方法时,迭代器会检查modCount和expectedModCount是否一致。

public E next() {
    checkForComodification(); // 检查是否被修改
    ...
}

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

3、调用iterator.remove()

如果通过迭代器的remove()方法删除集合的元素,此时迭代器iterator会同步更新modCount和expectedModCount,确保一致性。

public void remove() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    // 删除元素并更新 modCount
    ArrayList.this.remove(size - 1);
    cursor--;
    expectedModCount = modCount; // 同步更新
}

⚠️注意:如果直接使用list.remove(),modCount会增加,下次再调用的时候,会抛异常。

2.4、适用场景

1.单线程下的结构修改检测

该机制不仅适用于多线程环境,也适用于单线程中在迭代过程中直接修改集合的情况。

2.多线程下的并发修改

在多线程环境下,如果多个线程同时修改集合的元素,也可能导致modCount与expectedModCount不一致。

此时,迭代器的并发检查机制可以检测到这种冲突,但并不能完全解决线程安全问题。需要结合线程安全集合(如CopyOnWriteArrayList)或同步机制。

3、解决方案

3.1、迭代器的remove()

Iterator<Element> it = list.iterator();
while (it.hasNext()) {
    Element e = it.next();
    if (someCondition(e)) {
        it.remove(); // 安全地删除元素
    }
}

it.remove()是迭代器提供的方法,它会同步更新modCount和expectedModCount,避免异常。

3.2、普通for循环 + 控制索引

for (int i = 0; i < list.size(); i++) {
    Element e = list.get(i);
    if (someCondition(e)) {
        list.remove(i);
        i--; // 删除后索引前移
    }
}
  • 注意:删除元素后需要调整索引,防止跳过元素。

3.3、CopyOnWriteArrayList

如果确实需要在遍历过程中频繁修改集合,可以使用线程安全的CopyOnWriteArrayList:

List<Element> list = new CopyOnWriteArrayList<>();
for (Element e : list) {
    if (someCondition(e)) {
        list.remove(e); // 不会抛出异常
    }
}
  • 该集合在修改时会复制底层数组,避免直接修改共享数据,但性能开销较大。

3.4、Collections.synchronizedlist(list)

在多线程环境中使用Collections.synchronizedList时,遍历(Iteration)并修改列表的过程中,必须手动加锁,以确保线程安全。

这是因为在默认情况下,Collections.synchronizedList的同步机制仅覆盖其方法调用,但不包括迭代器(Iterator)的线程安全性

1、为什么需要手动加锁?

  • Collections.synchronizedList的同步机制
  • Collections.synchronizedList返回的列表是一个线程安全的包装类,其所有方法(如add、remove、get等)都通过synchronized关键字加锁,确保单个方法调用的线程安全

2.迭代器(Iterator)的线程安全性缺失

  • 虽然列表的方法是线程安全的,但迭代器本身并不是线程安全的
  • 例如:如果线程 A 正在遍历列表(使用iterator()),而线程 B 同时修改了列表(如add或remove),即使这些方法是同步的,迭代器也可能抛出ConcurrentModficationException,或者看到不一致的数据状态。

代码示例如下:

List<String> list = Collections.synchronizedList(new ArrayList<>());

// 添加元素
list.add("A");
list.add("B");

// 遍历时手动加锁
synchronized (list) {
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if (item.equals("A")) {
            iterator.remove(); // 安全地移除元素
        }
    }
}

小结:

使用迭代器提供的remove()方法、避免在for-each中直接修改集合,或选择适合的集合类型(如CopyOnWriteArrayList),可以有效避免此类异常。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • springboot实现简单的消息对话的示例代码

    springboot实现简单的消息对话的示例代码

    本文主要介绍了springboot实现简单的消息对话的示例代码,可以使用WebSocket技术,WebSocket是一种在客户端和服务器之间提供实时双向通信的协议,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • 详解使用spring boot admin监控spring cloud应用程序

    详解使用spring boot admin监控spring cloud应用程序

    本篇文章主要介绍了详解使用spring boot admin监控spring cloud应用程序,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • SpringBoot+easypoi实现数据的Excel导出

    SpringBoot+easypoi实现数据的Excel导出

    这篇文章主要为大家详细介绍了SpringBoot+easypoi实现数据的Excel导出,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • 详解Spring框架---IOC装配Bean

    详解Spring框架---IOC装配Bean

    本篇文章主要介绍了详解Spring框架---IOC装配Bean,提供了三种方式实例化Bean,具有一定的参考价值,有兴趣的可以了解一下。
    2017-03-03
  • Spring JPA的实体属性类型转换器并反序列化工具类详解

    Spring JPA的实体属性类型转换器并反序列化工具类详解

    这篇文章主要介绍了Spring JPA的实体属性类型转换器并反序列化工具类详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • IntelliJ IDEA 2018 最新激活码(截止到2018年1月30日)

    IntelliJ IDEA 2018 最新激活码(截止到2018年1月30日)

    这篇文章主要介绍了IntelliJ IDEA 2018 最新激活码(截止到2018年1月30日)的相关资料,需要的朋友可以参考下
    2018-01-01
  • JDK动态代理与CGLib动态代理的区别对比

    JDK动态代理与CGLib动态代理的区别对比

    今天小编就为大家分享一篇关于JDK动态代理与CGLib动态代理的区别对比,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • java自定义注解验证手机格式的实现示例

    java自定义注解验证手机格式的实现示例

    这篇文章主要介绍了java自定义注解验证手机格式的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • Java Class 加密工具 ClassFinal详解

    Java Class 加密工具 ClassFinal详解

    ClassFinal 是一款 java class 文件安全加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring-framework;可避免源码泄漏或字节码被反编译,这篇文章主要介绍了Java Class 加密工具 ClassFinal,需要的朋友可以参考下
    2023-03-03
  • Spring事务管理中关于数据库连接池详解

    Spring事务管理中关于数据库连接池详解

    事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就 回退到事务开始未进行操作的状态。事务管理是Spring框架中最为常用的功能之一,我们在使用Spring Boot开发应用时,大部分情况下也都需要使用事务
    2022-12-12

最新评论