并发修改异常源于modCount与expectedModCount失配,单线程遍历时调用集合原生增删也会触发;多线程应据读写比例选CopyOnWriteArrayList(读多写少)或ConcurrentHashMap(读写均衡)。

并发修改异常(ConcurrentModificationException)不是多线程专属问题,单线程遍历时直接调用集合的 remove() 或 add() 也会触发。核心在于“结构修改”与“迭代状态”失同步,而非线程数量本身。要真正规范多线程集合遍历安全,必须从 modCount 机制切入,再匹配场景选型。
理解本质:modCount 与 expectedModCount 的“信任断裂”
所有 fail-fast 集合(如 ArrayList、HashMap、HashSet)内部都维护一个 modCount 字段,记录结构性修改次数(增、删、清空等)。迭代器创建时会拷贝一份该值为 expectedModCount。每次调用 next() 或 remove() 前,都会执行 checkForComodification():
- 若
modCount != expectedModCount,立刻抛出异常 - 迭代器自己的
remove()方法会在删除后同步更新expectedModCount = modCount - 但集合自身的
remove()只改modCount,不碰迭代器的expectedModCount→ 下次next()必炸
单线程遍历删除:优先用迭代器 remove,禁用集合原生操作
这是最基础也最容易忽略的安全习惯。增强 for 循环(for (e : list))本质就是隐式使用迭代器,因此在循环体内写 list.remove(e) 等同于自毁。
- ✅ 正确:获取显式迭代器,用
it.remove() - ❌ 错误:在
while(it.hasNext())或增强 for 中调用list.remove() - ⚠️ 注意:
it.remove()只能删除上一次next()返回的元素,且每调用一次next()后最多调用一次remove()
多线程读多写少场景:CopyOnWriteArrayList / CopyOnWriteArraySet
适用于读操作远多于写操作、且允许短暂数据延迟的场景(如监听器列表、配置白名单)。其原理是写操作时复制整个底层数组,读操作永远访问快照,无锁、无阻塞。
- 读操作完全无同步开销,性能极高
- 写操作需加锁 + 数组复制,内存与 CPU 开销大,不适合高频写
- 迭代器是弱一致性视图:遍历时即使其他线程已修改,迭代器仍看到创建时刻的状态
- 不支持
null元素(CopyOnWriteArrayList)
多线程读写均衡或强一致性要求:ConcurrentHashMap / Collections.synchronizedXxx
当需要实时反映修改、或写操作较频繁时,应避免 CopyOnWrite 系列。
-
ConcurrentHashMap:分段锁(JDK8+ 改为 CAS + synchronized 链表/红黑树),支持高并发读写,迭代器弱一致但不抛CME -
Collections.synchronizedList(new ArrayList()):所有方法加synchronized,简单粗暴,但串行化严重,吞吐低 -
Vector:历史遗留,方法级同步,已不推荐 - 注意:
synchronizedList的迭代器仍需手动同步,否则遍历时写仍可能出错:synchronized(list) { Iterator it = list.iterator(); ... }
规范不在堆砌工具,而在理解每个选择背后的权衡:是接受读延迟换高并发,还是宁可串行也要强一致;是单线程逻辑疏忽,还是多线程资源争用。抓住 modCount 这根主线,就能看清所有方案的来龙去脉。

















