HashMap并发put会导致线程安全问题,根本原因在于其非原子性操作与缺乏同步机制:JDK 7中头插法扩容易形成环形链表引发死循环,JDK 8虽改用尾插法避免环形链表,但仍存在数据覆盖、size不准和竞态丢失等问题。

Java 中 Map 接口的 put 方法本身不保证线程安全,若在多线程环境下直接并发调用非线程安全的实现(如 HashMap),确实可能引发两种典型问题:一种是结构破坏导致的环形链表(尤其在 JDK 7 及之前版本扩容时),另一种是数据覆盖或丢失(JDK 8+ 虽避免了环形链表,但仍存在可见性与竞态问题)。
为什么 HashMap 并发 put 会出问题
HashMap 的 put 操作涉及数组索引计算、链表/红黑树插入、以及必要时的扩容(resize)。在 JDK 7 中,扩容采用头插法迁移链表节点,多线程同时触发扩容时,可能因执行顺序交错,使两个线程反复将彼此的节点插入到对方链表头部,最终形成环形链表——后续 get 或遍历时进入死循环。JDK 8 改为尾插法,消除了环形链表风险,但 put 仍不是原子操作:计算 hash、查找桶位、插入节点、检查是否转树、扩容判断等步骤间存在竞态窗口,会导致:
- 后写覆盖前写(同一 key 被不同线程先后 put,后者生效)
- size 计数不准(
modCount和size非 volatile,且更新未同步) - 部分节点未被插入或重复插入(尤其在扩容临界点)
如何快速定位是否是并发导致的问题
出现以下现象时,应优先怀疑 HashMap 并发修改:
- 应用卡顿或 CPU 占用持续 100%,线程堆栈中频繁出现
HashMap.get或HashMap.put的无限循环(JDK 7 典型表现) - 日志中偶发丢失 key-value 对,或读取不到刚
put的数据(无其他逻辑删除) - 使用 JMC 或 jstack 抓取线程快照,发现多个线程阻塞在
HashMap相关方法内,且堆栈深度异常大 - 单元测试在单线程下稳定,但多线程并发跑时结果不可重现、偶尔失败
正确替代方案与使用建议
不要试图给 HashMap 加锁修复,而应选用语义匹配的线程安全容器:
立即学习“Java免费学习笔记(深入)”;
-
高读低写场景:用
Collections.synchronizedMap(new HashMap()),它对所有 public 方法加synchronized,简单但吞吐量一般 -
高并发读写场景:首选
ConcurrentHashMap,JDK 8+ 采用 CAS + synchronized 分段锁(实际是基于 Node 的细粒度锁),支持高并发且保证线程安全的put、get、computeIfAbsent等操作 -
需要强一致性或复杂原子操作:考虑
ReentrantLock显式保护自定义 map 操作,或改用更高级结构(如 Guava 的CacheBuilder) -
只读 map 初始化后不再变更:用
Map.copyOf()(JDK 10+)或Collections.unmodifiableMap()包装,避免误写
排查工具与关键检查点
除代码审查外,可借助以下手段确认问题根源:
- 静态扫描:用 SonarQube 或 Alibaba Java Coding Guidelines 插件检测未同步的
HashMap成员变量 - 运行时监控:开启 JVM 参数
-XX:+UnlockDiagnosticVMOptions -XX:+PrintConcurrentLocks观察锁竞争 - 复现验证:编写最小并发测试(如 10 线程各 put 1w 次相同 key),对比
HashMap与ConcurrentHashMap的 size 和内容一致性 - 注意隐式共享:Spring Bean 默认单例,若在 service 层声明了
HashMap字段,多个请求线程会共用同一实例


















