jdk 1.7 hashmap 多线程扩容会形成环形链表,导致 get/put 等操作无限循环、cpu 飙高;根本原因是 transfer() 中头插法在并发下节点指针错乱,如 a→b 链表被两线程交叉迁移成 a↔b 环;jdk 1.8 改用尾插法并加锁优化,避免成环但仍未完全线程安全。

JDK 1.7 的 HashMap 在多线程扩容时确实会形成环形链表,进而导致 get()、put() 等操作无限遍历、CPU 占用飙高,但严格来说这不是“死锁”,而是“无限循环”——线程没被阻塞,而是在链表中打转。
头插法是环形链表的直接诱因
扩容核心逻辑在 transfer() 方法中,采用头插法迁移节点:
-
每次把旧链表节点 e 摘下后,执行
e.next = newTable[i]:即让当前节点指向新数组该槽位原来的头节点 -
再执行
newTable[i] = e:把 e 变成新槽位的新头节点 - 单线程下这会导致链表顺序反转(A→B→C 变为 C→B→A),本身无害
- 但多线程共享同一张旧表
table,而每个线程各自创建私有newTable,这就埋下竞态根源
两个线程并发扩容如何构造出环
假设旧数组某桶中链表为 A → B(A.next == B,B.next == null),线程 T1 和 T2 同时触发扩容:
- T1 开始执行,读取
e = A,缓存next = B,然后被系统暂停 - T2 继续运行,完成整个迁移:头插法下,先插 B 再插 A,得到新链表 B → A(B.next == A,A.next == null)
- T1 恢复执行,仍用旧状态:
e = A、next = B,且此时newTable[i]已是 B → A - T1 执行
A.next = newTable[i]→ A.next 指向 B(即 A → B) - 再执行
newTable[i] = A→ 新头变成 A,链表变为 A → B → A → B…
环一旦形成,哪些操作会卡住
环形链表不会立刻报错,但后续访问会陷入死循环:
-
get(key)查找时需遍历链表,若目标不在环内(如环是 A↔B,而 key 在 C),就会永远绕圈 -
put(key, value)若发生哈希冲突进入该桶,也会遍历链表计数或查找重复 key,同样卡住 -
size()或toString()等遍历全表的方法,可能在任意桶上触发无限循环 - CPU 使用率持续 100%,但线程状态仍是 RUNNABLE,不是 BLOCKED 或 WAITING
JDK 1.8 如何规避这个问题
1.8 改为尾插法迁移,链表顺序保持不变(A→B→C 迁移后仍是 A→B→C),从机制上避免了指针错乱成环的路径:
- 迁移时维护 last 引用,新节点总追加到链表末尾
- 即使多线程并发扩容,最多造成数据丢失或覆盖,不会构造出环
- 但注意:1.8 的 HashMap 依然不是线程安全的,并发 put 仍可能导致数据覆盖或不一致,只是不环
- 真正需要并发安全,应选
ConcurrentHashMap或加锁包装










