ConcurrentHashMap 迭代器不抛 ConcurrentModificationException 是因为其采用弱一致性设计,基于分段结构和 volatile 读实现非阻塞遍历,不校验 modCount;可能漏读、重复或返回 null,但保证已存在元素不消失。

为什么 ConcurrentHashMap 的迭代器不抛 ConcurrentModificationException
因为它的迭代器是弱一致性的——不是“实时快照”,也不是“完全隔离”,而是基于内部分段结构和 volatile 读的“尽力而为”遍历。它不阻塞写操作,也不校验修改计数(modCount),所以即使你一边 put() 一边 iterator().next(),也不会触发异常。
但注意:这不等于“安全看到所有修改”。它可能漏掉刚插入的元素,也可能重复看到某个元素(尤其在扩容时),甚至返回 null 值(如果遍历到正在被移除的节点)。
-
ConcurrentHashMap迭代器从不加锁,只依赖volatile变量和 UNSAFE 操作读取链表头、跳表层级等 - 遍历时若发生扩容,迭代器会尝试切换到新表,但切换过程本身不原子,导致中间状态可见
- 弱一致性仅保证“已存在的元素不会突然消失”,不保证“新增/删除立即可见”
CopyOnWriteArrayList 迭代器也弱一致,但原理完全不同
它用的是“写时复制”:每次 add() 或 remove() 都新建数组,而迭代器始终持有一个创建时的数组快照。所以遍历时绝对安全,永远不抛 ConcurrentModificationException,但也意味着你永远看不到遍历开始后发生的任何修改。
代价很直接:内存占用翻倍(旧数组暂不回收),且写操作延迟高(复制整个数组)。别在写多读少或元素很大的场景用它。
立即学习“Java免费学习笔记(深入)”;
- 适合读远多于写的场景,比如监听器列表、配置白名单
-
iterator()返回的ListIterator不支持set()和add(),调用会抛UnsupportedOperationException - 迭代器中的
hasNext()和next()是线程安全的,但结果反映的是迭代开始那一刻的状态
哪些容器的迭代器会抛 ConcurrentModificationException
标准非并发集合:ArrayList、HashMap、HashSet、LinkedList 等。它们的迭代器都检查 modCount 字段——只要结构修改次数和迭代器初始化时记录的不一致,下一次 next() 或 hasNext() 就抛异常。
这个异常不是同步机制,只是“快速失败”(fail-fast)策略,用来暴露错误逻辑,不是用来控制并发的。
- 单线程里也会触发:比如在
for-each循环中直接调用list.remove(x) - 不能靠捕获它来实现并发控制;它不提供任何同步语义
- 子类重写
add()但忘了更新modCount,也会导致误报或漏报
真要边遍历边修改,该选哪个容器
没有银弹。得看你要什么:
- 需要强一致性(看到全部最新变更)→ 改用显式同步:
synchronized(list) { ... },或Collections.synchronizedList()+ 手动加锁遍历 - 能接受“漏掉部分更新”+ 高并发读 →
ConcurrentHashMap或ConcurrentLinkedQueue - 读极多、写极少、元素不多 →
CopyOnWriteArrayList - 修改必须原子、且要精确控制可见性 → 别用迭代器,改用
computeIfAbsent()、replaceAll()等内置原子方法
最容易被忽略的一点:弱一致性不是免费的。它把复杂性下沉到了内存模型和 JVM 指令重排层面——你看到的“没抛异常”,背后是大量 volatile 读、Unsafe CAS 和对 CPU 缓存行的精细控制。别把它当成“随便改”的许可证。










