Java 集合中的 fail-fast 与 fail-safe 机制详解

在 Java 集合框架中,fail-fast(快速失败)和 fail-safe(安全失败)是两种应对 “迭代过程中集合结构被修改” 的核心机制,二者在原理、行为和适用场景上存在显著差异,理解它们对编写线程安全的集合操作代码至关重要。

一、fail-fast(快速失败)机制

1. 核心定义

fail-fast 机制是指在集合迭代过程中,若检测到集合的结构(如元素添加、删除)被修改(非迭代器自身的修改操作),会立即抛出ConcurrentModificationException异常,终止迭代过程,避免后续操作基于 “不一致的集合状态” 执行,实现 “快速暴露问题” 的目的。

2. 实现原理:基于modCount的版本校验

Java 中支持 fail-fast 的集合(如ArrayListHashMapHashSet等非线程安全集合)内部维护了一个修改计数器modCount,用于记录集合结构被修改的次数(每次 add/remove 操作都会使modCount++)。

当通过集合获取迭代器(如iterator()方法)时,迭代器会将当前modCount的值复制到自身的expectedModCount属性中。

在迭代过程中(如调用next()remove()),迭代器会先校验modCountexpectedModCount是否相等:

  • 若相等:正常执行迭代操作;
  • 若不相等:说明集合在迭代期间被修改(如其他线程添加元素,或当前线程使用集合的add()而非迭代器的remove()),立即抛出ConcurrentModificationException

3. 关键特点

  • 非线程安全:未对集合操作加锁,多线程环境下易触发异常;
  • 即时检测:修改后立即抛出异常,不允许 “脏迭代”;
  • 迭代器依赖集合本身:迭代过程中操作的是原集合,无额外内存开销;
  • 局限性:仅能检测 “结构性修改”(元素增减),无法检测 “元素值修改”(如list.set(index, value));且单线程中若通过迭代器的remove()修改,expectedModCount会同步更新,不会抛出异常(合法操作)。

4. 适用场景

  • 单线程环境下的集合迭代,需严格保证迭代期间集合不被意外修改;
  • 不需要并发读写的场景,追求迭代效率(无锁开销)。

5. 代码案例

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

public class FailFastDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        
        // 获取迭代器
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
            
            // 错误:使用集合的add()修改,触发fail-fast
            list.add("C"); // 执行到此处会抛出ConcurrentModificationException
        }
    }
}

二、fail-safe(安全失败)机制

1. 核心定义

fail-safe 机制是指在集合迭代过程中,即使集合结构被修改,迭代器也不会抛出异常,而是基于集合的 “快照”(Snapshot)进行迭代,保证迭代过程的安全性(不报错),但可能无法获取最新的集合数据。

2. 实现原理:基于 “读写分离” 与 “快照复制”

支持 fail-safe 的集合(如CopyOnWriteArrayListCopyOnWriteArraySetConcurrentHashMap)采用 “写时复制(Copy-On-Write, COW) ” 策略:

  • 读操作(迭代):直接读取集合的当前版本,无需加锁;
  • 写操作(add/remove):不直接修改原集合,而是先复制一份原集合的副本,在副本上执行修改操作,修改完成后再将集合的引用指向新副本,并更新版本号;
  • 迭代器快照:迭代器创建时,会记录当前集合的版本(或直接引用当前副本),后续迭代过程中操作的始终是这份 “快照”,即使原集合被修改(生成新副本),也不会影响迭代器的执行,因此不会抛出异常。

3. 关键特点

  • 线程安全:通过 “写时复制” 实现读写分离,无需显式加锁(底层通过ReentrantLock保证写操作的原子性);
  • 不抛异常:迭代期间集合被修改,仅影响新副本,迭代器基于旧快照执行,不会触发异常;
  • 数据一致性弱:迭代结果可能是 “过期数据”(快照与最新集合状态不一致);
  • 内存开销大:写操作需复制整个集合,若集合元素量大,会占用额外内存,且复制过程耗时;
  • 写操作效率低:频繁写操作会导致频繁复制,性能下降。

4. 适用场景

  • 多线程环境下的 “读多写少” 场景(如日志收集、配置缓存),允许迭代结果存在短暂延迟;
  • 不需要实时获取最新数据,更注重迭代过程安全性(不报错)的场景。

5. 代码案例

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

public class FailSafeDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");
        
        // 获取迭代器(此时快照为["A","B"])
        Iterator<String> iterator = list.iterator();
        
        // 另一个线程修改集合(生成新副本["A","B","C"])
        new Thread(() -> list.add("C")).start();
        
        // 迭代操作:基于旧快照执行,不会抛出异常,也不会获取到"C"
        while (iterator.hasNext()) {
            System.out.println(iterator.next()); // 输出"A"、"B"
        }
        
        // 打印最新集合:能获取到"C"
        System.out.println("最新集合:" + list); // 输出"最新集合:[A, B, C]"
    }
}

三、fail-fast 与 fail-safe 机制对比

对比维度 fail-fast 机制 fail-safe 机制
异常抛出 迭代中修改集合,立即抛ConcurrentModificationException 迭代中修改集合,不抛异常
数据一致性 迭代结果与集合实时状态一致(若未修改) 迭代结果基于快照,可能与最新状态不一致
线程安全性 非线程安全(多线程易出错) 线程安全(读写分离)
实现原理 基于modCountexpectedModCount校验 基于 “写时复制” 与快照迭代
内存开销 无额外开销(操作原集合) 写操作需复制集合,内存开销大
性能(读 / 写) 读操作快(无锁),写操作快(无复制) 读操作快,写操作慢(复制耗时)
适用集合 ArrayListHashMapHashSet等非线程安全集合 CopyOnWriteArrayListConcurrentHashMap等线程安全集合

四、机制选择总结

  1. 优先选 fail-fast

    单线程环境、不需要并发读写、追求实时数据一致性和低内存开销时(如普通业务逻辑中的列表遍历)。

  2. 优先选 fail-safe

    多线程 “读多写少” 环境、允许迭代结果延迟、注重迭代安全性(不报错)时(如高并发下的日志查询、配置读取)。

  3. 注意事项:

    • fail-fast 机制仅为 “检测机制”,不能替代线程安全控制(多线程下需额外加锁,如Collections.synchronizedList());
    • fail-safe 机制的 “安全性” 是 “不抛异常”,而非 “数据实时一致”,需根据业务容忍度选择。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐