hashmap不会导致真正死锁,而是多线程扩容时因头插法(jdk 1.7)或无锁并发引发环形链表,造成get/put无限循环、cpu 100%、线程状态全为runnable。

HashMap 本身不会产生真正意义上的死锁,它不涉及锁的获取与等待,所以不符合死锁的四个必要条件。所谓“HashMap 死锁”,其实是多线程并发扩容时引发的环形链表 + 无限循环,表现为 CPU 飙高、线程卡在 get() 或 put() 的链表遍历逻辑中,状态全是 RUNNABLE,不是 BLOCKED 或 WAITING。
看 jstack 堆栈锁定嫌疑线程
线上 CPU 拉满时,第一时间执行:
-
jstack -l <pid> > jstack.log</pid>,搜索HashMap.get、HashMap.put、HashMap.size等方法调用 - 重点观察:是否多个线程堆栈都停在相同行号,比如
while (e != null) { e = e.next; } - 确认线程状态全为
RUNNABLE,且无任何locked或waiting for monitor entry字样 - 配合
top -H -p <pid></pid>找出 CPU 最高的线程 tid,转成十六进制(nid),再在 jstack 中定位它是否卡在 HashMap 遍历里
结合 JDK 版本判断成因
JDK 1.7 和 1.8 的行为差异直接影响排查方向:
- JDK 1.7:扩容使用头插法,
transfer()中两步操作e.next = newTable[i]和newTable[i] = e在多线程交叉执行下极易形成 A→B→A 的闭环 - JDK 1.8:改用尾插法,避免了环形链表,但仍是非线程安全的——并发
put可能丢数据、size()不准确、或触发ConcurrentModificationException - 无论哪个版本,只要看到
RUNNABLE + 链表遍历卡死,就该怀疑是 HashMap 被多线程共享修改,而不是锁竞争问题
验证与快速止血方案
确认问题后,不要只停留在复现,要推动可落地的改进:
- 立即替换:把全局共享的
HashMap改为ConcurrentHashMap,它通过分段锁(JDK 7)或 CAS + synchronized(JDK 8+)保障线程安全 - 若需强一致性写入,考虑加外部锁(如
synchronized或ReentrantLock),但注意粒度,避免串行化瓶颈 - 本地缓存场景可搭配
computeIfAbsent等原子方法,减少手动同步 - 测试阶段可用
jcstress工具做并发压力验证,比单纯跑多线程put/get更能暴露竞态边界
这类问题隐蔽性强,但线索清晰——CPU 高 + RUNNABLE + HashMap 方法内循环,就是最典型的环形链表信号。早识别、快替换、少依赖经验直觉,靠工具和版本特性说话。











