HashMap 头尾插和并发环链问题如何理解?

考查重点:迁移交错与并发安全。JDK7 并发扩容交错改 next 可成环,头插本身不必然成环;JDK8变迁移方式仍不线程安全

本题目录

参考答案

核心回答

HashMap 的头插、尾插是链表连接方式;并发环链则是多个线程无同步迁移同一批节点,交错改写 next 指针造成的结构破坏。正常的单线程头插本身不会把无环链表变成环。

以 OpenJDK 7u80 为例,普通新增节点采用桶首插入,扩容迁移也把旧节点头插到新桶,可能反转同一目标桶的链表。多个线程并发扩容时,一个线程保存的“下一个节点”可能已被另一个线程重新连接,最终形成环,某些沿该桶遍历的查询便可能无法结束。

OpenJDK 8u412 的普通链表新增使用尾部追加,扩容则按新增的一位拆成两组,并保持各组相对顺序,避开了这条经典的头插反转路径。但 HashMap 仍非线程安全,尾插不会防止丢条目、计数竞争或读取不一致;共享更新应使用并发容器或统一同步。

一个两节点交错,环是怎样形成的

假设旧桶为 A→B→null,A、B 是不同键,扩容后仍落在同一个目标桶。两个线程各有自己的新数组,但迁移操作复用了同一批旧节点:

  1. 线程一处理 A,先保存“下一个是 B”,随后暂停。
  2. 线程二完整迁移这个桶。先把 A 接到自己的空桶,再把 B 插到 A 前面,旧节点关系变为 B→A→null。
  3. 线程一恢复,把 A 接到自己的空桶,然后沿之前保存的 B 继续。此时从 B 读到的 next 已经是线程二写入的 A。
  4. 线程一处理 B 时,保存“下一个是 A”,把 B 放到 A 前面。接着又处理了一次 A,将 A 的 next 接到当前桶首 B。
  5. 线程一的新桶因此变成 A→B→A,形成环。

关键在于:新数组是各线程的,节点对象却是共享的;线程一保存的局部引用,并没有保存一份不会变化的链表快照。查找一个落在该桶但不存在的键时,若遍历逻辑一直沿 next,就可能在环中无限循环。并不是每次并发 put 都必然形成环,命中首节点的查询也未必表现为卡死。

JDK 8 为什么不能简化成“尾插就安全”

普通插入和扩容迁移是两处不同逻辑。JDK 8 普通 put 会先检查是否存在相等键,已有键更新值,只有新键才追加节点;树桶走树的处理路径。扩容时维护高低两组的头尾指针,分别接到新表,而不是把所有节点简单接到一个新尾部。

保持分组内顺序减少了旧反转迁移相关的问题,但这些操作本身仍没有 CAS 或完整加锁协议。两个线程都判断同一桶为空后赋值,仍可能覆盖入口;size 的递增也不是并发原子计数。甚至自行写一套尾插迁移,只要没有解决共享指针竞争,就不能据此宣称线程安全。

此外,分组内相对顺序不是 HashMap 的全局迭代顺序契约。扩容会改变桶分布,程序不能依赖偶然观察到的遍历顺序。

追问:线上出现卡死或丢数据,怎么判断

先确认实际 JDK 版本和具体访问路径,检查是否有共享 HashMap 被无同步修改。历史版本若出现长时间卡在桶链遍历、CPU 持续忙等现象,线程栈和结构证据可以帮助判断是否涉及环链;不能只凭 CPU 高就断言是这个问题。

修复应消除无保护共享修改,例如使用 ConcurrentHashMap,或者让所有相关读写遵守同一同步协议。fail-fast 仅是尽力发现某些违规修改,不保证检测这种结构问题。升级 JDK 有价值,但不能代替修正并发访问方式。

参考资料

相关面经

4 篇

小红书

AI Native 全栈开发工程师
日常实习一面、二面
查看原面经 →
HashMap 头插 / 尾插以及并发环链问题

去哪儿

AI 应用开发工程师
查看原面经 →
并发操作有什么问题怎么解决

百度

后端开发工程师(AI / Agent 方向)
秋招一面
查看原面经 →
是线程安全的吗?如果不是怎么保证线程安全?

法大大

Java 开发工程师
实习一面
查看原面经 →
详细讲一下JDK7中HashMap头插法的循环列表问题

相关题目

4 题