JDK 7 的 HashMap 用数组 + 链表,扩容时 transfer 方法采用头插法:遍历旧桶链表,逐个把节点插到新表对应桶的头部,因此扩容后链表顺序反转。单线程下这没问题,但并发扩容时会成环:假设桶上有 A→B,线程 1 处理到 A 时被挂起(已执行 next = A.next 即 B,但 A 尚未迁移),线程 2 完成整个扩容,新表中顺序变为 B→A;线程 1 恢复后继续头插,A 指向新表头(B),下一轮又把 B 头插到 A 前——B.next 被改回 A,形成 A↔B 的环。此后对该桶的 get 陷入死循环,CPU 100%。

JDK 8 的修复:扩容改为尾插法保持相对顺序,并且用 (e.hash & oldCap) 把旧桶节点拆成「低位链」和「高位链」两条,不再反转,消除了成环条件。但要强调:JDK 8 的 HashMap 依然不是线程安全的——并发 put 仍会丢数据、size 不准,树化逻辑也未同步,只是不再死循环。并发场景的正确答案始终是 ConcurrentHashMap 或 Collections.synchronizedMap。追问方向:为什么头插法当初被选用(作者假设新插入的是热点,且实现简单)、ConcurrentHashMap 在 JDK 7 分段锁与 JDK 8 CAS+synchronized 的演进。

(约 440 字)