concurrenthashmap遍历时修改会成环,因其弱一致性迭代器禁止结构性修改,否则cas失败、迁移未完成或红黑树转链表时指针错乱,导致next指向自身或前驱形成单向环,引发无限循环与oom。

Java中因误用ConcurrentHashMap等并发容器引发死循环,最终导致OOM(Out of Memory),这类问题虽不常见,但一旦发生往往难以排查,核心原因不是容器本身有bug,而是违背了其设计约束——尤其是在遍历过程中执行结构性修改操作,可能触发内部链表/红黑树成环,使迭代器无限循环,持续占用CPU与堆内存,最终拖垮JVM。
为什么ConcurrentHashMap遍历时修改会成环?
ConcurrentHashMap(JDK 8+)采用分段锁+CAS+红黑树的混合结构。它的迭代器是弱一致性的(weakly consistent),不抛ConcurrentModificationException,但不保证看到最新修改,也不允许在遍历中做put/remove等结构性变更。当线程一边遍历Node链表,另一边在同个桶内执行put或remove,可能因CAS失败重试、迁移未完成、或红黑树转链表时节点指针错乱,导致next指针指向自身或前驱节点,形成单向环。此时get()、keySet().iterator().hasNext()等操作就会卡死在循环里。
哪些典型误用会触发这个问题?
- 在for-each或Iterator遍历中调用put()/remove():例如“遍历map清理过期key”,直接在循环体内remove(key);
- 用computeIfAbsent/computeIfPresent时在BiFunction里修改当前map:回调函数内又调用了put或clear;
- 多线程环境下共享同一个ConcurrentHashMap,一个线程遍历,另一个线程高频rehash或扩容:尤其在低版本JDK(如8u40前)中迁移逻辑存在竞态窗口;
- 将ConcurrentHashMap当作普通HashMap使用,套用HashMap的“先查后删”惯用法:如if (map.containsKey(k)) map.remove(k),在高并发下可能被其他线程干扰状态。
如何安全地边遍历边修改?
没有“安全的遍历中修改”捷径,必须拆分为两阶段操作:
- 收集待操作key,再批量处理:遍历keySet()或entrySet(),把要删的key存入临时集合(如CopyOnWriteArraySet),遍历结束后调用map.keySet().removeAll(toRemove);
- 用replaceAll()或compute()系列原子方法:它们内部已加锁或CAS保障一致性,例如map.replaceAll((k, v) -> isExpired(v) ? null : v) 会安全清除过期值;
- 对高频读写场景,考虑用更细粒度控制:比如按业务逻辑拆分多个ConcurrentHashMap,或改用StampedLock保护的普通HashMap(读多写少时性能未必差);
- 避免在computeXXX回调中嵌套修改同一map:回调应纯函数化,如有依赖,提取为外部计算后再传入。
如何快速识别和规避风险?
线上出现CPU 100% + OOM dump显示大量Thread处于TIMED_WAITING但堆内存持续增长,且线程栈频繁出现ConcurrentHashMap$Traverser.next()、TreeNode.find()等调用,基本可锁定该问题。预防上:
- 静态扫描工具(如Alibaba Java Coding Guidelines)能检测for-each中remove;
- 单元测试加入多线程压力遍历+修改场景,观察是否hang住;
- JDK 9+ 的ConcurrentHashMap新增mappingCount()替代size(),避免size()在扩容中反复遍历;
- 日志中警惕“ConcurrentHashMap was modified during iteration”类提示(虽不抛异常,但可自定义Wrapper打点预警)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











