JDK 1.7 HashMap 多线程扩容时因头插法导致环形链表,引发 get/put 等操作无限循环、CPU 飙高;JDK 1.8 改用尾插法并优化扩容机制,从根源消除死循环,但仍非线程安全,应直接替换为 ConcurrentHashMap。

直接换容器,别修 HashMap。
死循环只在 JDK 1.7 及之前存在
JDK 1.7 的 transfer() 方法用头插法扩容,多线程同时操作容易把链表搞成环形结构——get() 或 iterator() 遍历时就会无限循环,CPU 拉满。JDK 1.8 彻底改用尾插法,加上红黑树和扩容过程的原子性控制,环形链表已从机制上消除,不会再发生死循环。
但注意:不等于线程安全。JDK 1.8 的 HashMap 仍存在数据覆盖、丢失等并发问题,只是“不死循环”了。
别试图给 HashMap 加锁来“修复”
同步单个 put() 或整个方法体是无效的:
- 扩容逻辑(
resize())本身包含多步非原子操作,加锁无法覆盖所有竞态点 - 两个线程可能先后触发扩容,中间状态交错,头插残留风险仍在(即使 JDK 1.8 已不用头插,但手动模拟或误用旧逻辑仍可能出错)
- 锁粒度粗会导致性能断崖式下降,且无法解决复合操作(如先查后写)的竞态
正确做法:用真正为并发设计的容器
两种主流选择,按场景选:
-
ConcurrentHashMap:首选。JDK 1.8 使用 CAS + synchronized on node(锁单个桶),读操作无锁,写冲突仅限局部;支持
computeIfAbsent()、merge()等原子复合操作 - Collections.synchronizedMap(new HashMap()):全局一把锁,简单但吞吐低;仅适合读极多、写极少、QPS 很低的配置类缓存场景
避免踩坑:
- 别把
ConcurrentHashMap强转成HashMap或赋值给裸Map类型传给老框架 - 别手写“先
get再put”,必须用computeIfAbsent等原子方法封装 - 别以为“只读就安全”——JDK 1.7 中并发
get遇到正在扩容的线程,照样可能卡死
一句话总结
HashMap 不是“差点就安全”,而是根本没为并发设计。遇到并发场景,第一步不是调优,是替换:用 ConcurrentHashMap 替掉所有共享的 HashMap 实例,问题自然消失。


















