首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深度剖析 HashMap:从 JDK 1.7 死循环到 1.8 高低位映射优化

深度剖析 HashMap:从 JDK 1.7 死循环到 1.8 高低位映射优化

作者头像
用户11987541
发布2026-01-12 14:35:55
发布2026-01-12 14:35:55
100
举报

作者:[予枫]

发布时间:2026年1月

分类:Java 后端 / 底层原理


一、 引言:哈希冲突与 HashMap 的使命

在计算机科学中,哈希表通过哈希函数将 Key 映射到数组下标,实现 O(1) 的查找效率。然而,由于哈希函数输出空间有限,哈希冲突(Hash Collision) 避不可免。

常见的解决冲突方法包括:

  • 链地址法(Separate Chaining):HashMap 采用的核心策略。
  • 开放定址法(Open Addressing):如线性探测、平方探测。
  • 再哈希法(Rehashing):使用多个哈希函数。
  • 建立公共溢出区

HashMap 在单线程环境下表现卓越,但在多线程这片“深水区”,它就像一个没有交通灯的十字路口,极易引发致命灾难。


二、 HashMap 的五大线程安全陷阱

多线程同时修改同一个 HashMap 实例时,会引发以下问题:

  1. 扩容死循环(JDK 1.7):最著名的 Bug,会导致 CPU 飙升至 100%。
  2. 数据丢失/覆盖(1.7 & 1.8 共有):多个线程同时 put 时,由于没有加锁,计算出的索引若相同,后者的值会覆盖前者。
  3. Size 计算不准size++ 操作非原子性,并发下会导致计数不一致。
  4. 扩容覆盖(JDK 1.8):多线程同时扩容生成多个新数组,最终只有最后一个线程的数组会被保留,其余线程插入的数据随之丢失。
  5. 快速失败(Fail-Fast):在迭代过程中若有其他线程修改结构,会立即抛出 ConcurrentModificationException

三、 深度复现:JDK 1.7 的“死亡之环”

1. 源码现场

JDK 1.7 扩容的核心在于 transfer 方法,其罪魁祸首是头插法(Head Insertion)

代码语言:javascript
复制
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; // 【关键点 1】记录下一个节点
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];      // 【关键点 2】头插到新表
                newTable[i] = e;           
                e = next;                  // 【关键点 3】移动到下一个
            } while (e != null);
        }
    }
}
2. 场景分析

假设原链表为 A -> B -> null

  • 线程 T1 执行到记录 next = B 后被挂起。此时 T1 视角:e = A, next = B
  • 线程 T2 完成整个扩容,由于头插法,新链表变为 B -> A -> null。此时 A 的 next 已指向 null,而 B 的 next 指向了 A。
  • 线程 T1 恢复
    1. 处理 A:将 A 插入新表,e 变为 B。
    2. 处理 B:此时由于 T2 修改了指针,B.next 变成了 A。T1 记录 next = A,将 B 插入 A 之前。
    3. 再次处理 A:此时 e = AA.next 被设为当前头节点 B。
    • 结局B.next = AA.next = B环形链表诞生
3. 后果

当后续调用 get() 落入此桶时,while 遍历将永不停止,导致 CPU 占用率瞬间达到 100%。


四、 JDK 1.8 的救赎:高低位映射全解析

JDK 1.8 废弃了头插法,改用尾插法并引入了高低位映射(High-Low Mapping)

1. 数学基石:2 的幂次方

HashMap 容量 n 始终为 2^k。扩容时,新容量是旧容量的 2 倍。

  • 索引计算:Index = (n-1) \& hash
  • 奇妙现象:扩容后,掩码(Mask)仅在高位多出一个 1。这意味着元素新索引只有两种可能:原位置原位置 + 旧容量
2. 核心算法:四指针分流

1.8 使用 loHead, loTail(低位链表)和 hiHead, hiTail(高位链表)进行分选:

  • 判断条件(e.hash & oldCap) == 0
  • 低位(0):留在原位置。
  • 高位(1):搬移到 index + oldCap 位置。
3. 进阶:红黑树的 split 细节

若桶内是红黑树,扩容时调用 TreeNode.split()

  • 同样按高低位拆分为两个双向链表。
  • 去树化:若拆分后长度 \le 6,转回普通链表。
  • 树化:若长度 > 6

五、 总结与最佳实践

为什么 1.8 的设计更优?

特性

1.8 高低位映射

核心价值

位运算优化

(hash & oldCap)

极速判定,无需 rehash,性能翻倍。

尾插法

保持原序

彻底解决死循环问题。

树结构拆分

split 逻辑

保证扩容后大桶依然具备 $O(\log n)$ 的检索效率。

解决方案

在多线程环境下,请务必遵守:

  1. ConcurrentHashMap(首选):采用 CAS + synchronized 细粒度锁,支持多线程协同扩容,是生产环境的最佳选择。
  2. Collections.synchronizedMap:全表加锁,性能较低。

结语:

理解 HashMap 的演进不仅仅是为了面试,更是为了学习其背后精妙的位运算设计与并发处理思路。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 引言:哈希冲突与 HashMap 的使命
  • 二、 HashMap 的五大线程安全陷阱
  • 三、 深度复现:JDK 1.7 的“死亡之环”
    • 1. 源码现场
    • 2. 场景分析
    • 3. 后果
  • 四、 JDK 1.8 的救赎:高低位映射全解析
    • 1. 数学基石:2 的幂次方
    • 2. 核心算法:四指针分流
    • 3. 进阶:红黑树的 split 细节
  • 五、 总结与最佳实践
    • 为什么 1.8 的设计更优?
    • 解决方案
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档