当前位置: 首页 > news >正文

并发场景下使用 HashMap 可能出现死循环的原因?

在并发场景下使用 HashMap 可能出现死循环,主要原因是 JDK 1.7 及之前版本中 HashMap 的扩容机制(transfer 方法)采用「头插法」迁移链表节点,且未考虑线程安全,导致多线程并发扩容时链表形成环形结构,进而在后续操作(如 get 或 put)中触发无限循环。

具体原因分析(以 JDK 1.7 为例)

HashMap 的底层结构是「数组 + 链表」,当元素数量超过阈值(容量 * 负载因子)时,会触发扩容(数组长度翻倍),并通过 transfer 方法将旧数组的元素迁移到新数组。死循环的根源就在 transfer 方法的并发执行中

1. 扩容的核心步骤(transfer 方法)

迁移链表节点时,JDK 1.7 采用「头插法」:将旧链表的节点按逆序插入新链表(新节点插入到链表头部)。伪代码如下:

void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null; // 释放旧数组的引用// 遍历旧链表,迁移每个节点到新数组do {Entry<K,V> next = e.next; // 记录下一个节点int i = indexFor(e.hash, newCapacity); // 计算新索引e.next = newTable[i]; // 头插法:新节点的next指向新链表的头newTable[i] = e; // 将新节点设为新链表的头e = next; // 处理下一个节点} while (e != null);}}
}
2. 并发扩容导致链表成环

当两个线程同时对同一个 HashMap 进行扩容时,可能因「头插法」和线程调度顺序导致链表节点的 next 指针形成环:

  • 步骤 1:线程 A 和线程 B 同时开始扩容,都读取到旧数组中某条链表(如 e -> a -> be 是头节点)。
  • 步骤 2:线程 A 执行到 Entry<K,V> next = e.next 后暂停(此时 next 指向 a)。
  • 步骤 3:线程 B 正常完成扩容,将旧链表 e -> a -> b 迁移为新链表 b -> a -> e(头插法逆序)。
  • 步骤 4:线程 A 恢复执行,此时它持有的 e 仍指向旧的 e,但 e.next 已被线程 B 修改为 null(因为线程 B 已迁移完 e 并释放了旧引用)。
  • 步骤 5:线程 A 继续迁移 ee.next 指向新链表的头(此时新链表在 A 看来是空,所以 e.next = null),并将 e 设为新链表头。
  • 步骤 6:线程 A 处理 next(即 a),计算新索引后,a.next 指向当前新链表头 e,并将 a 设为新链表头。此时链表变为 a -> e
  • 步骤 7:线程 A 处理 a 的下一个节点(即 e,因为线程 B 中 a.next 已改为 e),e.next 指向当前新链表头 a,导致 e -> a 形成环(a.next = e 且 e.next = a)。
3. 死循环的触发

当链表形成环后,后续对该链表的操作(如 get 查找不存在的元素)会陷入无限循环(遍历链表时永远无法到达尾部),导致 CPU 占用率飙升至 100%。

JDK 1.8 的改进

JDK 1.8 中 HashMap 的扩容机制进行了优化:

  • 改用「尾插法」迁移链表节点(保持原链表顺序),避免了逆序插入导致的环。
扩容逻辑的代码重构(resize 方法)

JDK 1.8 将扩容逻辑从 transfer 方法整合到 resize 方法中,代码更简洁且避免了旧版本的潜在风险:

  • 迁移节点时,通过 next 指针记录下一个节点,确保遍历顺序稳定。
  • 对于每个链表,先找到迁移后新链表的头节点和尾节点,再将整个链表批量迁移到新数组,减少指针操作的并发风险。

核心代码片段(简化)

final Node<K,V>[] resize() {// ... 计算新容量、新阈值等省略 ...Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab; // 直接替换旧数组引用for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null; // 释放旧数组引用if (e.next == null) {// 单个节点直接迁移newTab[e.hash & (newCap - 1)] = e;} else if (e instanceof TreeNode) {// 红黑树迁移(单独处理)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);} else {// 链表迁移(尾插法核心)Node<K,V> loHead = null, loTail = null; // 原索引位置的链表Node<K,V> hiHead = null, hiTail = null; // 新索引位置的链表Node<K,V> next;do {next = e.next;// 判断节点应留在原索引还是迁移到新索引if ((e.hash & oldCap) == 0) {// 尾插法:更新尾节点指针if (loTail == null) loHead = e;else loTail.next = e;loTail = e;} else {if (hiTail == null) hiHead = e;else hiTail.next = e;hiTail = e;}} while ((e = next) != null);// 将两个链表放入新数组的对应位置if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}return newTab;
}

关键改进

  • 通过 loTail 和 hiTail 指针记录链表尾部,迁移时保持节点顺序(尾插法)。
  • 链表被拆分为两个子链表(原索引和新索引),批量迁移而非逐个节点插入,减少指针操作次数。
 引入红黑树优化,减少长链表的遍历风险

JDK 1.8 中,当链表长度超过阈值(默认 8)且数组容量 ≥ 64 时,链表会转换为红黑树。

  • 红黑树的遍历是有序的,即使在极端情况下出现并发问题,也不会像链表那样陷入无限循环(红黑树有自平衡机制,遍历有明确终止条件)。

总结

并发场景下 HashMap 出现死循环的根本原因是 JDK 1.7 及之前的「头插法」扩容机制在多线程并发时导致链表成环。解决办法是:

  • 并发场景下使用线程安全的 ConcurrentHashMap(推荐)。
  • 或通过 Collections.synchronizedMap 包装 HashMap(性能较差)。

但 HashMap 仍不是线程安全的(并发 put 可能导致数据覆盖、丢失等问题),只是解决了死循环问题。避免在多线程环境中直接使用 HashMap

http://www.dtcms.com/a/396130.html

相关文章:

  • 单链表/双链表/循环链表
  • 如何网站专题策划志愿服务网站开发
  • 宁波建设监理协会网站中国建设招标工程网站
  • PyQt5 中 LineEdit 控件数据的批量存储与读取
  • 商城网站需要多少空间四川建设厅的网站
  • 陕西建设银行缴费网站全球网络营销公司排名
  • 超快轻量级离线翻译服务器MTranServer在腾讯云轻量应用服务器上的全流程部署指南
  • 山东网站优化推广手机软件开发学什么
  • 自己做网站吗天元建设集团有限公司济宁分公司
  • 网站怎么推广引流巩义市网站建设培训班
  • 网站建设珠海 新盈科技做影视网站风险大吗
  • 开发网站需要问什么金华网络公司网站建设
  • 企业网站推广17有免费的网站服务器吗
  • 清欢互联网网站建设中国交通建设工程监督管理局网站
  • 二刷DC: 6靶场
  • Redis存储对象选择String还是Hash呢?怎么选择?
  • 中国安能建设集团有网站网络公司哪个效果好
  • 昆山科技网站建设无锡网站排名优化报价
  • 做文献综述的文章用什么网站蚌埠网页设计
  • 网站开发报价明细表深圳市建网站公司
  • 自己做的网站被攻击了uc网页浏览器网页版
  • 审计追溯困难会对企业带来哪些风险
  • 一维差分(扫描线)之区间重叠
  • 电子商务网站怎么做二级子域名查询入口
  • 怎么查看网站虚拟空间wordpress中文编辑器插件安装
  • 17网站一起做网店新塘亚马逊关键词排名提升
  • 网站优化qq群南宁seo优势
  • 做电影网站失败了yeezy橙色
  • 硬件驱动——I.MX6ULL裸机启动(9)(RGBLCD相关设置)
  • php网站开发报告书wordpress手机主题mip