作者:[予枫]
发布时间:2026年1月
分类:Java 后端 / 底层原理
在计算机科学中,哈希表通过哈希函数将 Key 映射到数组下标,实现 O(1) 的查找效率。然而,由于哈希函数输出空间有限,哈希冲突(Hash Collision) 避不可免。
常见的解决冲突方法包括:
HashMap 在单线程环境下表现卓越,但在多线程这片“深水区”,它就像一个没有交通灯的十字路口,极易引发致命灾难。
多线程同时修改同一个 HashMap 实例时,会引发以下问题:
put 时,由于没有加锁,计算出的索引若相同,后者的值会覆盖前者。
size++ 操作非原子性,并发下会导致计数不一致。
ConcurrentModificationException。
JDK 1.7 扩容的核心在于 transfer 方法,其罪魁祸首是头插法(Head Insertion)。
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);
}
}
}假设原链表为 A -> B -> null。
next = B 后被挂起。此时 T1 视角:e = A, next = B。
B -> A -> null。此时 A 的 next 已指向 null,而 B 的 next 指向了 A。
e 变为 B。
B.next 变成了 A。T1 记录 next = A,将 B 插入 A 之前。
e = A,A.next 被设为当前头节点 B。
B.next = A 且 A.next = B,环形链表诞生。
当后续调用 get() 落入此桶时,while 遍历将永不停止,导致 CPU 占用率瞬间达到 100%。
JDK 1.8 废弃了头插法,改用尾插法并引入了高低位映射(High-Low Mapping)。
HashMap 容量 n 始终为 2^k。扩容时,新容量是旧容量的 2 倍。
1.8 使用 loHead, loTail(低位链表)和 hiHead, hiTail(高位链表)进行分选:
(e.hash & oldCap) == 0。
index + oldCap 位置。
split 细节若桶内是红黑树,扩容时调用 TreeNode.split():
特性 | 1.8 高低位映射 | 核心价值 |
|---|---|---|
位运算优化 | (hash & oldCap) | 极速判定,无需 rehash,性能翻倍。 |
尾插法 | 保持原序 | 彻底解决死循环问题。 |
树结构拆分 | split 逻辑 | 保证扩容后大桶依然具备 $O(\log n)$ 的检索效率。 |
在多线程环境下,请务必遵守:
synchronized 细粒度锁,支持多线程协同扩容,是生产环境的最佳选择。
结语:
理解 HashMap 的演进不仅仅是为了面试,更是为了学习其背后精妙的位运算设计与并发处理思路。