文章目录
- 一、先说结论:一个宁可误杀不肯放过,一个稳如老狗
- 二、fail-fast:宁可错杀,不可放过
- 1. 它是怎么"快失败"的?
- 2. 正确的删除姿势
- 三、fail-safe:你们改你们的,我玩我的
- 1. 它是怎么"安全失败"的?
- 2. fail-safe 的代价
- 四、回到全貌:一图记住
- 五、回答技巧与点评
- 标准回答
- 加分回答
- 面试官点评
个人网站
你一定遇到过这个场景:遍历 ArrayList 的时候顺手删了个元素,结果程序直接炸了,甩给你一个ConcurrentModificationException。但换成 CopyOnWriteArrayList,同样的操作却安然无恙。
凭啥一个炸一个不炸?这背后就是 Java 集合的两种保护机制——fail-fast和fail-safe。
一、先说结论:一个宁可误杀不肯放过,一个稳如老狗
|| 维度 | fail-fast | fail-safe |
||------|-----------|-----------|
|| 策略 | 发现结构被修改,立即抛异常 | 复制一份数据操作,对修改无感 |
|| 代表 | ArrayList、HashMap、HashSet | CopyOnWriteArrayList、ConcurrentHashMap |
|| 抛异常 | ✅ConcurrentModificationException| ❌ 不抛 |
|| 底层原理 |modCount计数器比对 | 操作副本,原数据不动 |
|| 性能 | 高(直接操作原数据) | 有拷贝开销 |
|| 数据一致性 | 强(发现问题立刻暴露) | 弱(读到的可能是旧数据) |
一句话记住:fail-fast 是警报器,发现问题立刻响;fail-safe 是隔离区,问题关在门外你听不到。
二、fail-fast:宁可错杀,不可放过
1. 它是怎么"快失败"的?
fail-fast 机制的原理特别简单——每次遍历前检查集合是否被修改过,修改过就炸。
底层靠一个叫modCount的计数器:集合每次增删改,modCount就 +1。遍历开始时记下当前的modCount,每次取下一个元素前比对一下,发现不一致,立马抛ConcurrentModificationException。
List<String>list=newArrayList<>(Arrays.asList("张三","李四","王五"));for(Stringname:list){if(name.equals("李四")){list.remove(name);// 💥 ConcurrentModificationException!}}你可能会问:我单线程操作啊,又没并发,凭啥报错?因为 fail-fast不管你是谁,只要遍历过程中modCount变了,它就认为出事了。它宁可误报,也不让问题悄悄溜过去。
这就像火灾警报器——有时候只是烤面包冒了点烟,它也叫。但万一真着火了,你希望它叫不叫?当然希望叫。这就是 fail-fast 的设计哲学。
2. 正确的删除姿势
如果遍历时确实要删元素,用 Iterator 的remove()方法:
List<String>list=newArrayList<>(Arrays.asList("张三","李四","王五"));Iterator<String>it=list.iterator();while(it.hasNext()){Stringname=it.next();if(name.equals("李四")){it.remove();// ✅ 正常删除,Iterator 会同步更新 modCount}}或者更简单,Java 8+ 直接用removeIf:
list.removeIf(name->name.equals("李四"));// ✅ 一行搞定三、fail-safe:你们改你们的,我玩我的
1. 它是怎么"安全失败"的?
fail-safe 的思路完全不同——它不去检测修改,而是直接操作副本,原数据怎么改都不关它的事。
以CopyOnWriteArrayList为例,每次遍历时,迭代器拿的是当时集合的一个快照:
CopyOnWriteArrayList<String>list=newCopyOnWriteArrayList<>(Arrays.asList("张三","李四","王五"));for(Stringname:list){if(name.equals("李四")){list.remove(name);// ✅ 不报错!但遍历的仍然是原来的快照}}不报错,但有代价——遍历过程中"李四"已经被删了,但迭代器还在快照上跑,所以你遍历时仍然能看到"李四"。这就是所谓弱一致性:读到的可能是旧数据。
再看ConcurrentHashMap,它不复制整个集合,而是用分段锁 + Node 链表的方式,让遍历和修改互不干扰:
ConcurrentHashMap<String,Integer>map=newConcurrentHashMap<>();map.put("a",1);map.put("b",2);for(Stringkey:map.keySet()){map.remove("a");// ✅ 不报错,遍历不受影响}2. fail-safe 的代价
fail-safe 看起来很美好,但它不是银弹:
- CopyOnWriteArrayList:每次写操作都复制整个数组,写多读少的场景性能灾难
- ConcurrentHashMap:遍历时可能读到已删除的数据,一致性要求高的场景要慎重
这就好比你拍了张照片慢慢看,但照片里的人已经走了——你看到的是过去的影子,不是现在的真实。
四、回到全貌:一图记住
fail-fast fail-safe ────────────────────────────────────────────── ArrayList、HashMap CopyOnWriteArrayList HashSet、LinkedList ConcurrentHashMap modCount 检测修改 操作副本/快照 发现修改立刻抛异常 不抛异常 性能高,数据强一致 有拷贝开销,弱一致性 单线程遍历时优先选择 并发场景下优先选择 口诀:fast 快报错,safe 不报错; fast 操作原数据,safe 操作副本; 单线程用 fast,多线程用 safe。五、回答技巧与点评
标准回答
fail-fast 是 Java 集合的一种快速失败机制,遍历过程中如果集合结构被修改,会立即抛出 ConcurrentModificationException,底层通过 modCount 计数器实现,ArrayList、HashMap 等属于此类。fail-safe 是安全失败机制,遍历时操作集合的副本或快照,不会因为原集合被修改而抛异常,CopyOnWriteArrayList、ConcurrentHashMap 属于此类,但可能读到旧数据,具有弱一致性。
加分回答
- 提到本质区别:fail-fast 是"发现问题立刻暴露",fail-safe 是"用副本隔离问题",根本取舍是性能 vs 一致性
- 提到并发场景选择:多线程环境下用 ConcurrentHashSet 而非同步包装的 HashSet,就是因为前者是 fail-safe 设计,不需要加锁遍历
- 提到单线程避坑:单线程遍历删除要用 Iterator.remove() 或 removeIf,不是 fail-fast 有 bug,是你用错了姿势
面试官点评
这道题考的不是你背不背得出来两个名词,而是你理解不理解 Java 集合框架的设计取舍。如果只说"一个报错一个不报错"就太浅了——能讲出 modCount 原理、副本机制、强弱一致性,才算真懂。如果还能结合并发场景说出该选哪个集合,面试官会认为你有实际开发经验,这是加分项。
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪