关键词:HashMap、hash 算法、红黑树、链表、扩容死链、源码、面试
适合人群:Java 初中高级工程师 · 面试冲刺 · 代码调优 · 架构设计
阅读时长:40 min(≈ 6000 字)
版本环境:JDK 17(源码行号对应 jdk-17+35,同时回顾 JDK 7 死链)
1. 开场白:面试四连击,能抗算我输
- “HashMap 的 hash 方法为什么要把高 16 位异或到低 16 位?”
- “链表转红黑树阈值为什么是 8 而不是 7 或 9?”
- “JDK 7 扩容死链如何形成?请现场画图。”
- “红黑树退链表阈值 6,那为什么不是 7?”
阿里 P8 面完 100 人,能把行号、 CPU 分支预测、泊松分布串起来的不到 5 个。
线上事故:金融系统 JDK 7 时代 512 线程并发 put,触发死链,CPU 100%,FullGC 每 2 min 一次,回滚包车。
背完本篇,你能手写红黑树旋转、复现死链环路、给出 3 种规避方案,让面试官沉默。
2. 知识骨架:HashMap 全图一张带走
HashMap
├─Node[] table // 底层数组
├─int threshold // 扩容阈值 = capacity * loadFactor
├─final float loadFactor // 负载因子,默认 0.75f
├─static final int TREEIFY_THRESHOLD = 8
├─static final int UNTREEIFY_THRESHOLD = 6
├─static final int MIN_TREEIFY_CAPACITY = 64
└─static final int hash(Object key) // 扰动函数
读写流程:
hash → (n-1) & hash 定位桶 → 链表 or 树 → 冲突拉链 → 扩容 → 重新散列
3. 身世档案:核心参数一表打尽
| 字段/常量 | 含义 | 默认值/备注 |
|---|---|---|
| DEFAULT_INITIAL_CAPACITY | 初始容量 | 16 |
| MAXIMUM_CAPACITY | 最大容量 | 1 << 30 |
| DEFAULT_LOAD_FACTOR | 负载因子 | 0.75f |
| TREEIFY_THRESHOLD | 链表转红黑树阈值 | 8 |
| UNTREEIFY_THRESHOLD | 红黑树退链表阈值 | 6 |
| MIN_TREEIFY_CAPACITY | 树化最小表长度 | 64 |
4. 原理解码:源码逐行,行号指路
4.1 hash 算法:高 16 位异或,降低冲突(行号 340)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
目的:让高 16 位也参与
(n-1)&hash运算,减少表长较小时的碰撞。
4.2 putVal 全流程(行号 631)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // ① 懒初始化
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // ② 桶空
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // ③ 桶首匹配
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // ④ 红黑树
else { // ⑤ 链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 8-1=7
treeifyBin(tab, hash); // 行号 657
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize(); // ⑥ 扩容
afterNodeInsertion(evict);
return null;
}
4.3 treeifyBin():链表转红黑树(行号 757)
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 表长 < 64 优先扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab); // 真正树化
}
}
先树化链表节点,再构建红黑树;表长不足 64 则扩容退避。
4.4 扩容 resize():2 倍容量 + 重散列(行号 699)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = threshold << 1; // 2 倍阈值
}
else if (threshold > 0)
newCap = threshold; // 指定初始容量
else {
newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { // 重散列
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // help GC
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 原索引 + oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
亮点:利用
(e.hash & oldCap) == 0判断新桶位置,避免重新取模。
4.5 红黑树退化链表(行号 798)
final boolean untreeify(HashMap<K,V> map, Node<K,V>[] tab) {
// 树节点数 ≤ 6 时退链表
}
阈值 6 而非 7,避免在 6-7 之间频繁树化/退树造成 CPU 抖动(泊松分布 < 0.00005)。
5. 实战复现:3 段代码 + 压测
5.1 哈希冲突测试:String 相近 key
Map<String,Integer> map = new HashMap<>(16);
for (int i = 0; i < 20; i++) {
map.put("Aa" + i, i); // hash("Aa")=2112 相同前缀
}
System.out.println(map.size()); // 20
JProfiler 查看:同桶内链表长度 = 20,未树化(表长 16 < 64)。
5.2 树化与退化观测
Map<Integer,String> map = new HashMap<>(64); // 指定 64 避免扩容
// 生成 20 个 hash 冲突的 key
for (int i = 0; i < 20; i++) {
int hash = 12345; // 强制冲突
map.put(new Key(hash, i), "v" + i);
}
当 i = 8 时断点:进入 treeifyBin();
删除至 6 个节点:触发 untreeify()。
5. JDK 7 死链复现(单线程模拟)
// 使用 JDK 7 源码拷贝
Map<Integer,Integer> map = new java7.HashHashMap<>(2, 0.75f);
for (int i = 0; i < 5; i++) map.put(i, i); // 触发多次扩容
断点观测:
transfer 方法内 Entry next = e.next; 与 e.next = newTable[i]; 形成环,导致 get() 死循环。
6. 线上事故:负载因子 1.0 引发 CPU 飙高
背景
日志统计 Map<String,Long> 默认负载因子 0.75,数据量 2000 万,运维误改为 1.0 想“省内存”。
现象
CPU 使用率 +50%,RT P99 从 50 ms 涨到 500 ms。
根因
负载因子 1.0 导致链表长度平均 8-12,大量桶遍历;红黑树虽缓解,但 hash 冲突高时仍退化。
复盘
- 压测:0.75 vs 1.0,QPS 下降 40%。
- 修复:改回 0.75,并预扩容
new HashMap<>(1 << 24)。 - 防呆:代码托管平台增加 .properties 参数校验规则。
7. 面试 10 连击:答案 + 行号
| 问题 | 答案 |
|---|---|
| 1. hash 方法高 16 位异或目的? | 减少表长较小时的碰撞(行号 340) |
| 2. 链表转树阈值? | 8(行号 657) |
| 3. 树退链表阈值? | 6(行号 798) |
| 4. 最小树化表长? | 64(行号 757) |
| 5. 为何阈值 8 和 6 不连续? | 避免 6-7 之间频繁树化/退树抖动 |
| 6. 扩容倍数? | 2 倍(行号 703) |
| 7. 重新散列算法? | e.hash & oldCap == 0 判断新桶(行号 725) |
| 8. JDK 7 死链根本原因? | 头插法 + 并发 transfer 形成环 |
| 9. 负载因子 1.0 影响? | 链表长,查询退化到 O(n) |
| 10. 如何线程安全? | ConcurrentHashMap |
8. 总结升华:一张脑图 + 三句话口诀
[脑图文字版]
中央:HashMap
├─hash:高 16 位异或
├─链表:≥8 && 表≥64 转树
├─扩容:2 倍,重散列
└─死链:JDK 7 头插环
口诀:
“高 16 位异或低,八六阈值树退链;二倍扩容重散列,七版死链要记全。”
9. 下篇预告
下一篇《早期线程安全集合源码速读:Hashtable、Vector、Collections.synchronizedXxx》将带你全表锁、并发压测、兼容迁移,敬请期待!
10. 互动专区
你在生产环境踩过 HashMap 树化或死链坑吗?评论区贴出堆栈 / GC 图,一起源码级排查!
浙公网安备 33010602011771号