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

深入解析JDK1.8 HashMap优化之道

好的,我们来深入分析HashMap的核心机制,重点关注从 JDK 1.7 到 1.8 的重大改进,特别是解决死循环问题和引入高低位映射优化。

1. JDK 1.7HashMap的结构与潜在问题

在 JDK 1.7 中,HashMap采用数组 + 链表的结构:

  • 数组 (table): 存储链表的头节点。
  • 链表 (Entry): 解决哈希冲突,相同哈希值的元素存储在同一个链表中,采用头插法(新元素插入链表头部)。
// JDK 1.7 简化版 Entry 结构 static class Entry<K, V> implements Map.Entry<K, V> { final K key; V value; Entry<K, V> next; // 指向下一个节点的指针 int hash; // ... 构造方法等 }
1.1 并发扩容导致的死循环问题

HashMap需要扩容(通常是当size > threshold = capacity * loadFactor)时,会执行resize()操作:

  1. 创建新数组: 容量翻倍。
  2. 迁移元素: 遍历旧数组的每个桶(链表),重新计算每个元素在新数组中的位置index = (newCapacity - 1) & hash
  3. 迁移方式: 使用头插法将元素插入新数组的对应链表中。

问题根源 - 头插法与并发

  • 在并发环境下,多个线程可能同时触发resize()
  • 头插法会导致链表元素的迁移顺序发生反转(旧链表 A->B->C,迁移后在新链表变成 C->B->A)。
  • 如果两个线程同时迁移同一个链表,由于线程执行顺序的不确定性,可能导致链表形成环形结构
// 简化版 JDK 1.7 resize 中的迁移逻辑 (易产生死循环) void transfer(Entry[] newTable) { for (Entry<K, V> e : oldTable) { while (null != e) { Entry<K, V> next = e.next; // 线程1执行到这里暂停 e.next = newTable[e.hash & (newCapacity - 1)]; // 头插法 newTable[e.hash & (newCapacity - 1)] = e; e = next; } } }
  • 线程1执行Entry<K, V> next = e.next;后暂停。假设此时e指向节点 A,next指向节点 B。
  • 线程2完成了整个链表的迁移,假设迁移后链表是 B->A(头插法导致顺序反转)。
  • 线程1恢复执行:
    • e.next = newTable[index];:将 A 的next指向新数组index位置的节点(此时是 B,因为线程2迁移后 B 是头节点)。
    • newTable[index] = e;:将 A 设置为新数组index位置的头节点。现在链表是 A->B。
    • e = next;e指向 B。
    • 下一轮循环:next = e.next = B.next。在线程2迁移后的链表中,B 的next指向 A!所以next指向 A。
    • e.next = newTable[index];:将 B 的next指向新数组index位置的头节点(此时是 A)。现在链表变成 B->A->B,环形链表形成
  • 后续调用get()访问这个桶时,遍历链表会陷入死循环。

2. JDK 1.8HashMap的重大优化

JDK 1.8 对HashMap进行了显著重构:

  • 数据结构:数组 + 链表 / 红黑树。当链表长度超过阈值(默认为 8)且桶数组长度达到一定大小(默认为 64)时,链表转换为红黑树,以提高查询效率($$O(n) \to O(\log n)$$)。
  • 迁移方式: 改用尾插法(新元素插入链表尾部),保持元素顺序不变,从根本上解决了并发扩容死循环问题(但HashMap本身仍非线程安全)。
  • 高低位映射优化: 这是扩容迁移过程中的关键性能优化。
2.1 高低位映射优化详解

在 JDK 1.8 的resize()方法中,迁移元素时不再简单地重新计算每个元素的哈希值(newCapacity - 1) & hash,而是利用了一个巧妙的性质:元素在新数组中的位置只可能是原位置i或者i + oldCapacity

数学原理

  • 设旧容量为oldCap,新容量newCap = 2 * oldCapHashMap容量总是 2 的幂)。
  • 计算桶索引的公式是index = (capacity - 1) & hash
  • 由于capacity是 2 的幂,capacity - 1的二进制形式是低位全 1,高位全 0(例如oldCap = 16 (10000)oldCap - 1 = 15 (01111))。
  • 新容量newCap - 1的二进制是比oldCap - 1多一个高位 1(例如newCap = 32 (100000)newCap - 1 = 31 (011111))。
  • 计算元素在新数组中的位置: $$index_{new} = (newCap - 1) & hash = (2 \times oldCap - 1) & hash$$
  • 比较元素在旧数组中的位置: $$index_{old} = (oldCap - 1) & hash$$
  • 关键在于hash值在log2(oldCap)(即oldCap对应的那个二进制位)的值:
    • 如果该位是0:则(newCap - 1) & hash(oldCap - 1) & hash的结果完全相同(因为新增的高位在&操作中是 0,不影响低位结果)。所以元素应留在原位index_{old}
    • 如果该位是1:则(newCap - 1) & hash的结果等于(oldCap - 1) & hash + oldCap(因为新增的高位是 1,&后相当于在index_{old}基础上加了一个oldCap)。所以元素应迁移到新位置index_{old} + oldCap

优化实现: JDK 1.8 在迁移时,直接将一个旧桶 (oldTab[j]) 拆分成两个新链表:

  • 低位链表 (loHead,loTail): 存放哈希值满足(hash & oldCap) == 0的元素(第log2(oldCap)位为 0),它们将留在新数组的j位置。
  • 高位链表 (hiHead,hiTail): 存放哈希值满足(hash & oldCap) != 0的元素(第log2(oldCap)位为 1),它们将迁移到新数组的j + oldCap位置。
// JDK 1.8 resize() 中迁移逻辑的简化 (包含高低位优化) Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // ... 其他初始化 for (int j = 0; j < oldCap; ++j) { Node<K, V> e; if ((e = oldTab[j]) != null) { Node<K, V> loHead = null, loTail = null; // 低位链表头尾 Node<K, V> hiHead = null, hiTail = null; // 高位链表头尾 do { Node<K, V> next = e.next; if ((e.hash & oldCap) == 0) { // 判断关键位是否为0 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { // 关键位为1 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 将两个链表放入新数组对应位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; // 低位链表留在原位 j } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; // 高位链表移到 j + oldCap } } }

优势:

  1. 避免重新计算哈希: 只需进行一次位运算(e.hash & oldCap)判断关键位,即可确定元素的新位置,效率远高于重新计算(newCap - 1) & hash
  2. 链表拆分高效: 遍历一次旧链表,同时构建两个新链表(低位和高位),迁移操作非常高效。
  3. 保持元素顺序: 使用尾插法构建新链表,保持了元素的相对顺序(对后续转换为红黑树也有利)。

3. 总结对比 (JDK 1.7 vs JDK 1.8)

特性JDK 1.7JDK 1.8改进点
数据结构数组 + 链表数组 + 链表 / 红黑树查询效率提升 (长链表 $$O(n) \to O(\log n)$$)
冲突解决插入法头插法尾插法解决并发扩容死循环,保持顺序
扩容迁移方式遍历链表,每个元素重新计算位置高低位映射优化,链表拆分成低位和高位避免重算哈希,迁移效率显著提升
线程安全非线程安全 (死循环问题)非线程安全 (但无死循环)更健壮,但仍需ConcurrentHashMap保证安全

JDK 1.8 的HashMap通过引入红黑树、尾插法和高低位映射优化,在解决 1.7 致命缺陷(死循环)的同时,显著提升了性能(尤其在哈希冲突严重和扩容时),是现代 Java 应用中更高效、更可靠的选择。理解这些底层机制对于编写高性能代码和排查问题至关重要。

http://www.jsqmd.com/news/327543/

相关文章:

  • Docker-Compose限制容器CPU/内存使用小记
  • 大数据预处理:自动化数据增强技术解析
  • Java毕设项目:基于springboot+BS构架的失物招领系统设计与实现(源码+文档,讲解、调试运行,定制等)
  • 为什么 AI 时代,SaaS 突然值钱了呢?
  • 网络安全入门基础-常用工具安装及使用(上)
  • Java毕设项目:基于Java的自驾游攻略查询系统的设计与实现(源码+文档,讲解、调试运行,定制等)
  • 提示工程架构师:用户交互优化的最新技术
  • 【毕业设计】基于Java的自驾游攻略查询系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • 前端新手必备:Gemini生成项目部署到Floudflare
  • 计算机Java毕设实战-基于springboot+BS构架的失物招领系统设计与实现失物信息管理【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • Java RESTful开发:从入门到精通
  • 2026年文博导览的新范式:从具身智能到知识共创的深度演进
  • Java计算机毕设之基于Java的自驾游攻略查询系统的设计与实现基于Java的自驾游攻略查询系统的设计与实现(完整前后端代码+说明文档+LW,调试定制等)
  • 深入解析Java栈帧机制
  • 学习日记day74
  • 全网最新免费降AI方法:工具+降AI工具实测
  • Excel MEDIAN函数终极指南:从基础语法到条件中值计算实战
  • 寒假学习10(HAL库1+模数电10)
  • Java毕设选题推荐:基于springboot+bs架构的浙江艾艺塑业设计公司网站设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 降AI率实操心得:5分钟搞定AI降重,从65%到14%的全过程复盘
  • 铁的居里点(770度就不被磁铁吸了)道理是什么?能不能精确计算出来?
  • 计算机Java毕设实战-基于springboot+bs架构的浙江艾艺塑业设计公司网站设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 2026年最好用的5款降AI工具+免费降AI方法【建议收藏】
  • “光能智测”太阳能预测技术——融合WRF-Solar与多源数据的短-中长期预报实战
  • 降AI实测:从85%到个位数,我只用了这3招(附工具清单)
  • 【课程设计/毕业设计】基于springboot+BS构架的失物招领系统设计与实现失物发布、招领管理、感谢信发表【附源码、数据库、万字文档】
  • 面向高质量SCI论文标准:深度挖掘遥感时空大数据价值、GeoAI可解释性建模与机理归因及高质量论文产出全链路实践技术
  • C++项目推荐-真正可以媲美redis的kv存储项目-包括性能如何逐步优化
  • Java毕设项目:基于JavaWeb的原色蛋糕商城的设计与实现(源码+文档,讲解、调试运行,定制等)
  • Kali蓝牙扫描以及配对具体指令