作者:[予枫]
发布时间:2026年1月
分类:Java 后端 / 底层原理
一、 引言:哈希冲突与 HashMap 的使命
在计算机科学中,哈希表通过哈希函数将 Key 映射到数组下标,实现 O(1) 的查找效率。然而,由于哈希函数输出空间有限,哈希冲突(Hash Collision) 避不可免。
常见的解决冲突方法包括:
-
链地址法(Separate Chaining):HashMap 采用的核心策略。
-
开放定址法(Open Addressing):如线性探测、平方探测。
-
再哈希法(Rehashing):使用多个哈希函数。
-
建立公共溢出区。
HashMap 在单线程环境下表现卓越,但在多线程这片"深水区",它就像一个没有交通灯的十字路口,极易引发致命灾难。
二、 HashMap 的五大线程安全陷阱
多线程同时修改同一个 HashMap 实例时,会引发以下问题:
-
扩容死循环(JDK 1.7):最著名的 Bug,会导致 CPU 飙升至 100%。
-
数据丢失/覆盖(1.7 & 1.8 共有) :多个线程同时
put时,由于没有加锁,计算出的索引若相同,后者的值会覆盖前者。 -
Size 计算不准 :
size++操作非原子性,并发下会导致计数不一致。 -
扩容覆盖(JDK 1.8):多线程同时扩容生成多个新数组,最终只有最后一个线程的数组会被保留,其余线程插入的数据随之丢失。
-
快速失败(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 恢复:
-
处理 A:将 A 插入新表,
e变为 B。 -
处理 B:此时由于 T2 修改了指针,
B.next变成了 A。T1 记录next = A,将 B 插入 A 之前。 -
再次处理 A:此时
e = A,A.next被设为当前头节点 B。
- 结局 :
B.next = A且A.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) 的检索效率。 |
解决方案
在多线程环境下,请务必遵守:
-
ConcurrentHashMap(首选) :采用 CAS +
synchronized细粒度锁,支持多线程协同扩容,是生产环境的最佳选择。 -
Collections.synchronizedMap:全表加锁,性能较低。
结语:
理解 HashMap 的演进不仅仅是为了面试,更是为了学习其背后精妙的位运算设计与并发处理思路。