fail-fast是Java集合的快速失败机制,指迭代器在遍历时检测到集合结构被外部修改(如add/remove),立即抛出ConcurrentModificationException;HashSet采用该机制,因其底层基于HashMap并维护modCount计数器,每次next()前校验一致性,旨在尽早暴露错误、防止数据错乱。

HashSet 遍历时调用 set.remove() 会触发 ConcurrentModificationException(CME),根本原因是其底层采用 fail-fast 机制检测结构性修改。
fail-fast 是什么,为什么 HashSet 要用它
fail-fast 不是并发安全机制,而是快速失败策略:当迭代器发现集合被外部线程或当前线程在迭代过程中修改了结构(如 add/remove 元素),就立即抛出 CME。HashSet 继承自 AbstractSet,底层用 HashMap 存储,其迭代器在构造时记录 modCount(修改计数),每次 next() 前校验该值是否变化。一旦不一致,立刻失败——目的是尽早暴露错误,避免数据错乱或无限循环等更隐蔽问题。
哪些操作会触发 CME
以下情况在遍历中直接调用会导致异常:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- 使用增强 for 循环(本质是 iterator)并调用
set.remove() - 显式获取 Iterator 后,用
set.remove()或set.add()修改集合 - 多个线程同时读写同一 HashSet(即使没显式迭代,也可能在迭代中途被改)
安全删除的正确做法
必须通过迭代器自身的 remove() 方法删除当前元素:
- 增强 for 循环不可用
remove(),应改用显式 Iterator -
Iterator.remove()会同步更新expectedModCount,与底层modCount保持一致 - 示例:
Iteratorit = set.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.startsWith("a")) it.remove(); // ✅ 安全
}
替代方案:需要并发修改怎么办
若场景确实需边遍历边增删(如过滤清理),可考虑:
-
CopyOnWriteArraySet:适合读多写少、遍历频繁的场景,但写操作开销大,不适用于大数据量 - 先收集待删元素,遍历结束后统一调用
set.removeAll(toRemove) - 加锁(如
synchronized(set))+ 手动控制,但破坏了 fail-fast 的初衷,且易引入死锁或性能瓶颈
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










