一、前言

在Java日常开发中,我们经常会遇到 ConcurrentModificationException(并发修改异常),绝大多数新手会疑惑:为什么遍历集合时删除元素会报错?多线程操作集合为什么有的报错有的不报错?

这一切的底层根源就是 Java 集合的两大容错机制:

  • Fail-Fast(快速失败):非线程安全集合默认机制,有错立即抛出异常

  • Fail-Safe(安全失败):线程安全并发集合机制,牺牲一致性保证安全

本文将彻底吃透这两个机制,解决开发报错、搞定面试高频问题。


二、基础概念:什么是并发修改?

很多同学有误区:并发修改 = 多线程修改,这是错误的!

Java 集合层面的并发修改定义:

通过迭代器(Iterator/增强for)遍历集合的过程中,对集合进行结构性修改(add/remove/clear)

✅ 包含两种场景:

  1. 单线程:增强for遍历中调用集合remove/add

  2. 多线程:一个线程遍历、一个线程修改


三、Fail-Fast 快速失败机制(重点)

3.1 机制定义

Fail-Fast(快速失败)ArrayList、LinkedList、HashMap、HashSet 等非线程安全集合的迭代保护机制。

核心特性

  • 遍历过程中一旦检测到集合结构被修改,立即抛出 ConcurrentModificationException

  • 不继续执行、不容忍异常、快速暴露问题

  • 目的:避免不可预知的脏数据、遍历错乱问题

3.2 底层核心原理(源码解析)

Fail-Fast 的核心依赖两个全局变量:

  1. modCount:集合结构性修改次数(add/remove/clear 都会 ++)

  2. expectedModCount:迭代器创建时,记录当前集合的 modCount

3.2.1 核心校验源码(ArrayList)

迭代器每次执行 next() 都会执行校验:

// ArrayList 迭代器核心校验方法
final void checkForComodification() {
    // 如果当前修改次数 != 迭代器初始化时的次数,直接抛异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

3.2.2 完整执行流程

  1. 创建集合,不断 add,modCount 自增

  2. 创建迭代器,expectedModCount = modCount

  3. 遍历途中调用集合 remove/addmodCount++

  4. 下次遍历执行 checkForComodification,校验失败抛异常

3.3 实战代码示例(触发Fail-Fast)

示例1:单线程增强for遍历删除(最常见报错)

import java.util.ArrayList;
import java.util.List;

/**
 * 单线程触发 Fail-Fast 机制
 * 报错原因:增强for遍历过程中,使用集合对象的remove方法修改结构
 */
public class FailFastSingleDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("C++");

        // 增强for底层是Iterator迭代器
        for (String language : list) {
            if ("Python".equals(language)) {
                // 集合对象删除:modCount ++,但迭代器expectedModCount不变
                list.remove(language);
            }
        }
    }
}

运行结果:直接抛出并发修改异常

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)

示例2:多线程场景触发Fail-Fast

import java.util.ArrayList;
import java.util.List;

/**
 * 多线程触发 Fail-Fast
 * 线程1遍历,线程2修改集合
 */
public class FailFastMultiDemo {
    private static final List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        list.add("Java");
        list.add("Go");
        list.add("Rust");

        // 遍历线程
        new Thread(() -> {
            for (String s : list) {
                System.out.println("遍历元素:" + s);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 修改线程
        new Thread(() -> {
            try {
                Thread.sleep(200);
                list.add("C#");
                System.out.println("新增元素成功");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

3.4 避坑:迭代器自带remove为什么不报错?

很多同学疑惑:同样是删除,迭代器的remove就不报错?

原因:迭代器删除后,会同步更新 expectedModCount,保证两端数值一致!

正确安全写法(单线程遍历删除)

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class FailFastCorrectDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("C++");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            if ("Python".equals(s)) {
                // 迭代器内部更新 expectedModCount,校验通过
                iterator.remove();
            }
        }
        System.out.println("最终集合:" + list);
    }
}

四、Fail-Safe 安全失败机制

4.1 机制定义

Fail-Safe(安全失败)CopyOnWriteArrayListCopyOnWriteArraySetConcurrentHashMap 等并发集合的机制。

核心特性

  • 遍历不操作原集合,而是操作集合快照副本

  • 遍历过程中原集合修改,不会影响副本,永远不抛出并发修改异常

  • 牺牲数据实时一致性,换取并发遍历安全

4.2 底层原理

  1. 迭代器初始化时,拷贝原集合数据生成新副本

  2. 所有遍历操作基于副本执行

  3. 原集合的 add/remove 只会修改原数组,不影响遍历副本

  4. 遍历结束,副本垃圾回收

4.3 优缺点总结

优点:并发遍历安全,无异常,适合多线程读场景

缺点

  • 每次修改需要拷贝数组,内存开销大、写性能差

  • 只能读取遍历初始化那一刻的数据,无法感知遍历中新增/删除的数据(弱一致性)

  • 迭代器不支持 add/remove 操作,会抛出不支持操作异常

4.4 实战代码示例(Fail-Safe无异常)

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Fail-Safe 安全失败机制演示
 * 多线程遍历+修改,全程无异常
 */
public class FailSafeDemo {
    public static void main(String[] args) {
        // 支持Fail-Safe的线程安全集合
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("C++");

        // 遍历线程
        new Thread(() -> {
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()) {
                String item = iterator.next();
                System.out.println("遍历元素:" + item);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("✅ 遍历完成,无任何异常");
        }).start();

        // 修改线程
        new Thread(() -> {
            try {
                Thread.sleep(200);
                list.add("Go语言");
                System.out.println("✅ 线程2新增元素成功");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果:全程无异常,遍历只会读取旧数据,新增数据不会被本次遍历读取

遍历元素:Java
遍历元素:Python
✅ 线程2新增元素成功
遍历元素:C++
✅ 遍历完成,无任何异常

五、Fail-Fast vs Fail-Safe 核心对比(面试必背)

对比维度 Fail-Fast 快速失败 Fail-Safe 安全失败
适用集合 ArrayList、HashMap、HashSet 等非线程安全集合 CopyOnWriteArrayList、ConcurrentHashMap 等并发集合
遍历数据源 直接遍历原集合 遍历集合快照副本
并发修改异常 会抛出 ConcurrentModificationException 不会抛出该异常
数据一致性 强一致性,实时感知修改 弱一致性,无法感知遍历中新修改的数据
性能开销 无拷贝,开销小,效率高 数组拷贝,开销大,写性能低
设计思想 快速暴露问题,避免隐患 牺牲一致性,保证并发安全
适用场景 单线程环境 多线程、读多写少场景

六、开发实战选型规范(生产可用)

  1. 单线程遍历删除:禁止增强for直接删集合,使用 迭代器自带remove()

  2. 多线程读多写少:优先使用 CopyOnWriteArrayList(Fail-Safe)

  3. 多线程写多读少:不使用CopyOnWrite(拷贝开销太大),使用 synchronized 加锁或 ConcurrentHashMap

  4. 追求数据强一致:使用Fail-Fast集合,自行控制并发锁

  5. 容忍短暂数据不一致:使用Fail-Safe并发集合,提升并发性能


七、面试高频问答(压轴)

Q1:Fail-Fast 机制的原理是什么?

集合维护一个 modCount 修改计数器,迭代器创建时记录 expectedModCount,遍历过程中校验两者是否一致,不一致则抛出并发修改异常,快速失败。

Q2:为什么迭代器remove不报错,集合remove就报错?

集合remove只会让 modCount++,不会修改迭代器的 expectedModCount;迭代器remove会同步更新expectedModCount,保证校验一致。

Q3:Fail-Safe 为什么不会抛并发修改异常?有什么缺点?

遍历基于集合快照副本,原集合修改不影响遍历。缺点是弱一致性、写操作内存开销大、迭代器不支持修改操作。

Q4:ConcurrentHashMap 是 Fail-Fast 还是 Fail-Safe?

JDK1.8+ ConcurrentHashMap 迭代器采用 弱一致性的Fail-Safe设计,不抛并发修改异常,无法感知遍历过程中的部分更新。


八、全文总结

  1. Fail-Fast:快速失败,强一致性,有错即抛异常,适用于单线程普通集合

  2. Fail-Safe:安全失败,弱一致性,快照遍历无异常,适用于多线程读多写少场景

  3. 开发中90%的遍历删除报错,都是误用了集合remove,而非迭代器remove

  4. 并发场景严禁乱用CopyOnWrite集合,写多场景性能会严重退化

更多推荐