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

作者:[予枫]

发布时间: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)

java 复制代码
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 的演进不仅仅是为了面试,更是为了学习其背后精妙的位运算设计与并发处理思路。

相关推荐
superman超哥几秒前
Rust 借用分割技巧:突破借用限制的精确访问
开发语言·后端·rust·编程语言·借用分割技巧·借用限制·精准访问
程序炼丹师1 分钟前
C++ 中的 std::tuple (元组)的使用
开发语言·c++
计算机学姐6 分钟前
基于SpringBoot的高校体育场馆预约系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·信息可视化·推荐算法
程序员佳佳6 分钟前
【万字硬核】从GPT-5.2到Sora2:深度解构多模态大模型的“物理直觉”与Python全栈落地指南(内含Banana2实测)
开发语言·python·gpt·chatgpt·ai作画·aigc·api
Coder_Boy_8 分钟前
基于SpringAI的在线考试系统设计-用户管理模块设计
java·大数据·人工智能·spring boot·spring cloud
不绝19112 分钟前
C#进阶——内存
开发语言·c#
风送雨13 分钟前
Go 语言进阶学习:第 1 周 —— 并发编程深度掌握
开发语言·学习·golang
小白不想白a14 分钟前
RabbitMQ监控
java·rabbitmq·java-rabbitmq
小北方城市网15 分钟前
第 5 课:服务网格(Istio)实战|大规模微服务的流量与安全治理体系
大数据·开发语言·人工智能·python·安全·微服务·istio
jghhh0116 分钟前
自适应信号时频处理方法MATLAB实现(适用于非线性非平稳信号)
开发语言·算法·matlab