跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

什么是Java的Fail-Fast机制_并发修改集合时的快速失败原理解析

ConcurrentModificationException 在单线程遍历时调用集合结构性修改方法(如list.remove())即触发,因迭代器的expectedModCount与集合modCount不匹配;多线程下应选用CopyOnWriteArrayList或ConcurrentHashMap等fail-safe容器。 ConcurrentModificationException 是怎么被触发的 它不是“并发”专属异常,而是集合结构被意外修改时的即时警报。核心就一句话:
modCount
和
expectedModCount
对不上了。前者是集合自己记的“改过几次”,后者是迭代器创建时抄下来的快照。只要遍历中途有人调了
add
、
remove
、
clear
这类结构性方法(哪怕单线程),下次调
next()
就会进
checkForComodification()
,然后直接抛
ConcurrentModificationException
。 for-each 循环本质就是
iterator()
+
while(hasNext())
,所以一样中招
Enumeration
不触发,因为它压根没
remove
方法,也不维护
expectedModCount
这个检测是“尽力而为”——JVM 不保证 100% 捕获所有并发修改,所以不能靠它做同步逻辑 单线程里为什么也会 fail-fast 很多人以为只有多线程才出问题,其实最常见场景就在自己写的 for-each 里删元素:
List list = new ArrayList<>(Arrays.asList("a", "b", "c")); for (String s : list) { if ("b".equals(s)) { list.remove(s); // ⚠️ 这里就炸了 } }
原因很简单:for-each 底层用的是迭代器,但你删元素用的是
list.remove()
,它只更新了
modCount
,没告诉迭代器——等下一轮
next()
调用时,校验失败,立刻中断。 正确做法是用迭代器自己的
remove()
方法,它会同步更新
expectedModCount
如果要删多个,别边遍历边删;先收集待删项,再用
removeAll()
用普通 for 循环从后往前删(
i--
)能绕过异常,但逻辑易错,不推荐作为首选方案 多线程下怎么安全地遍历并修改 ArrayList 别硬扛
ArrayList
+ 手动加锁——性能差、易死锁、还容易漏锁点。真要并发读写,换容器比修逻辑更靠谱: Eclipse导入Android或其他的JAVA项目的正确方法 WORD版 本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看 下载 立即学习 “ Java免费学习笔记(深入) ”; 读多写少:用
CopyOnWriteArrayList
,写操作复制数组,遍历永远看到快照,不抛异常,但内存开销大、写延迟高 读写均衡:用
ConcurrentHashMap
替代
HashMap
,它分段锁 + CAS,无
modCount
,天然不 fail-fast 必须用原生
ArrayList
?那就用
Collections.synchronizedList()
,但注意:迭代仍需手动同步整个块,否则还是可能出问题 fail-fast 和 fail-safe 的关键区别在哪 根本不在“快不快”,而在“看谁的数据”: fail-fast(如
ArrayList.iterator()
):盯着原始集合的
modCount
,改了就停,宁可中断也不读脏数据 fail-safe(如
ConcurrentHashMap.keySet().iterator()
或
CopyOnWriteArrayList.iterator()
):遍历的是快照或副本,原集合怎么改都影响不到当前迭代,但你也看不到那些新改的内容 没有银弹:fail-safe 避开了异常,却带来数据可见性问题;fail-fast 暴露了问题,但要求你主动处理结构变更逻辑 最容易被忽略的一点:很多开发者以为换成
ConcurrentHashMap
就万事大吉,但它对
keySet()
迭代器是 fail-safe,对
entrySet().iterator()
却仍是弱一致性——删掉的 entry 可能还在迭代中出现一次,这不是 bug,是设计使然。

相关文章