Java 集合如何实现 Fail-Fast 与 Fail-Safe?迭代器机制详解

你是否遇到过这样的异常?

java.util.ConcurrentModificationException

这其实是 Java 集合“自我保护”的一种机制 —— Fail-Fast。但在某些场景下我们又会发现某些集合在并发环境下并不会抛异常,这是为什么?本篇为你详解 Java 集合框架中 Fail-Fast 与 Fail-Safe 的设计原理与实现机制。


一、什么是 Fail-Fast 与 Fail-Safe?

✅ Fail-Fast(快速失败)

当多个线程对同一个集合进行结构性修改(如 add/remove),一旦检测到非当前线程修改,立即抛出异常,避免数据不一致。

常见集合:

  • ArrayList

  • HashSet

  • HashMap

  • LinkedList

✅ Fail-Safe(安全失败)

这种机制不会抛出异常,它会在遍历期间创建一个副本,从副本中读取数据,即便原集合被修改也不会出错。

常见集合:

  • CopyOnWriteArrayList

  • ConcurrentHashMap


二、Fail-Fast 的实现原理解析

核心字段:modCount(修改次数)

📦 示例(基于 ArrayList):

public class ArrayList<E> {
    protected transient int modCount = 0;
    public E remove(int index) {
        modCount++; // 每次结构性修改都会+1
        ...
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
        public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            ...
        }
    }
}

🧠 设计目的:

  • 防止脏读:遍历过程中检测集合是否被其他线程修改;

  • 性能优先:不加锁,只在问题发生时终止遍历,符合“快速失败”思想;


三、Fail-Safe 的底层机制剖析

💡 CopyOnWriteArrayList 的思路:

写操作发生时,复制原数组,修改副本,最后替换引用,读操作不加锁。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        Object[] newElements = Arrays.copyOf(elements, elements.length + 1);
        newElements[elements.length] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
  • 避免并发修改影响迭代;

  • 适用于 读多写少 场景;

  • 写时开销大,不适合频繁修改的集合场景;


💡 ConcurrentHashMap 的思路:

  • 使用 Segment 锁或 CAS + volatile 保证并发安全;

  • keySet().iterator() 返回的是弱一致性的迭代器;

  • 遍历期间允许并发修改,不会抛出异常;


四、实际开发中该如何选用?

场景

推荐集合

机制

特点

读多写少,迭代期间避免并发异常

CopyOnWriteArrayList

Fail-Safe

写开销大,读性能好

高并发读写 + Map结构

ConcurrentHashMap

Fail-Safe

支持高并发,弱一致性

单线程或明确同步控制下

ArrayList、HashMap

Fail-Fast

性能好,需避免并发修改

保证一致性,宁可失败

LinkedList、HashSet

Fail-Fast

适合用于数据结构修改频繁场景下的保护


五、常见面试问题与误区

1.为什么 Fail-Fast 不使用锁来避免并发问题?

答:Fail-Fast 是一种检测机制,不代表是线程安全的设计。使用锁反而会带来性能下降,而 Fail-Fast 的本意就是“不安全就立即报错”。

2.为什么 ConcurrentHashMap 的迭代器不抛异常?

答:它使用 弱一致性(Weakly Consistent)迭代器,即迭代期间的数据不一定是最新的,但不会抛异常。

3.CopyOnWriteArrayList 的 iterator 有什么性能隐患?

答:迭代的是快照,如果集合很大,复制开销会非常高;此外,如果频繁写入,会产生大量短生命周期的数组对象,GC 压力变大。


六、如何用 Iterator.remove()安全修改集合?

🚫 错误写法:使用 for-each直接删除元素

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String item : list) {
    if ("A".equals(item)) {
        list.remove(item); // 抛 ConcurrentModificationException
    }
}

✅ 正确写法:使用 Iterator.remove()

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("A".equals(item)) {
        iterator.remove(); // 安全地删除元素
    }
}

🔍 原理解析:

  • Iterator 内部会在调用 next() 后标记当前位置;

  • remove() 会将该位置的元素移除,并更新 modCount 与 expectedModCount;

  • 如果使用 list.remove() 则破坏了预期修改计数的一致性,从而触发 fail-fast 机制。


七、for-each与 Iterator的底层行为对比?

🌟 语法糖剖析:

for (String item : list) {
    // 实际是下面的写法:
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        ...
    }
}

❗误区警告:

  • 虽然 for-each 使用的是 Iterator,但你无法手动调用 remove(),因此不适合用于迭代删除场景;

  • 若在 for-each 中结构性修改集合(如 add/remove),仍然会抛 ConcurrentModificationException。

✅ 建议:

  • 遍历过程中需要修改集合,始终使用显式 Iterator

  • 如果使用 Stream 处理,请确保不在 map/filter/forEach 中修改原集合。


八、自定义集合如何支持 Fail-Fast 检测机制?

当我们实现一个自定义集合类时,如果希望其支持 Fail-Fast 行为,需要模仿 JDK 的方式,维护一个 modCount 修改次数字段。

🛠 示例:自定义集合类

public class MyList<E> extends AbstractList<E> {
    private List<E> internal = new ArrayList<>();
    protected transient int modCount = 0;
    @Override
    public E get(int index) {
        return internal.get(index);
    }
    @Override
    public int size() {
        return internal.size();
    }
    @Override
    public E remove(int index) {
        modCount++; // 修改次数+1
        return internal.remove(index);
    }
    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E> {
        private int cursor = 0;
        private int expectedModCount = modCount;
        @Override
        public boolean hasNext() {
            return cursor < size();
        }
        @Override
        public E next() {
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            return internal.get(cursor++);
        }
    }
}

✅ 核心实现要点:

  • 每次结构性修改时增加 modCount;

  • 迭代器内部缓存初始 modCount 到 expectedModCount;

  • 遍历过程中对比两者是否一致,若不一致则抛异常;


九、总结

  • Java 集合中的 Fail-Fast 和 Fail-Safe 是对并发修改行为的两种不同策略;

  • Fail-Fast 不是线程安全机制,而是一种错误检测机制;

  • 想要安全遍历集合,必须使用 Fail-Safe 容器或同步控制;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小健学 Java

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值