将用“搬家大混乱”的比喻,结合JDK 1.7的HashMap源码,为你揭示多线程死循环的产生原理。这个故事会让你像看悬疑剧一样轻松理解这个经典面试问题!
🧳 故事背景:搬家工人的链表反转陷阱
想象有两个搬家工人(线程T1和T2)在搬运一条项链(链表)。项链由珠子(节点)串联而成,顺序是A→B→C。他们的任务是把项链搬到新房间(扩容),但规则很奇葩:
必须倒序搬运(头插法)!新房间的项链顺序要变成C→B→A。
正常搬家(单线程)
- 工人拆下A珠 → 新房间:A
- 拆下B珠 → 新房间:B→A(B插在A前面)
- 拆下C珠 → 新房间:C→B→A
混乱搬家(多线程)
当两个工人同时搬运时:
⚠️ 一、JDK 1.7的死循环代码解析
关键代码:头插法扩容(Transfer方法)
java
Copy
void transfer(Entry[] newTable) {
for (Entry<K,V> e : oldTable) {
while (e != null) {
Entry<K,V> next = e.next; // ⚡️步骤1:记录下一个节点
int newIdx = hash(e.key) & (newCapacity-1);
e.next = newTable[newIdx]; // ⚡️步骤2:头插法(反转链表)
newTable[newIdx] = e; // ⚡️步骤3:新表头部指向当前节点
e = next; // ⚡️步骤4:处理下一个节点
}
}
}
死循环形成步骤(对照故事)
假设初始链表:头节点 → A → B → null
时刻0:
- T1执行:
e = A, next = B(记录B位置) → 暂停 - T2执行:完整搬运链表,新房间链表变为:
B → A → null
时刻1:
-
T1恢复执行(仍以为链表是
A→B→null):java Copy e.next = newTable[newIdx]; // 新表当前为空,A.next = null newTable[newIdx] = e; // 新表头部指向A e = next; // e = B(此时T2已把A.next改为B→A) -
T1继续处理B:
java Copy e.next = newTable[newIdx]; // B.next = A(形成B→A) newTable[newIdx] = B; // 新表头部指向B
最终链表:B → A → B(环状!)
💡 关键点:T1暂停时保留的
next=B已失效(T2修改后实际B.next=A),导致头插法产生环。
🔍 二、死循环的具象化演示
环形链表结构
当调用map.get(key)时:
-
程序从头部B开始遍历
-
访问B → 访问A → 访问B → 访问A...
-
CPU 100%(无限循环无法退出)
复现代码(JDK 1.7环境下运行)
java
Copy
public static void main(String[] args) throws InterruptedException {
Map<Integer, Integer> map = new HashMap<>(2, 0.75f);
map.put(1, 1); // 初始化链表
// 线程1:扩容时插入数据
Thread t1 = new Thread(() -> {
map.put(2, 2); // 触发扩容
});
// 线程2:同时触发扩容
Thread t2 = new Thread(() -> {
map.put(3, 3); // 同时触发扩容
});
t1.start();
t2.start();
t1.join();
t2.join();
// 此时调用get(1)可能陷入死循环!
System.out.println(map.get(1));
}
🛠️ 三、JDK 1.8的救赎:尾插法解决死循环
改进代码:尾插法(避免反转)
java
Copy
// JDK 1.8的扩容逻辑(Node替换Entry)
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 {
if (hiTail == null) hiHead = e;
else hiTail.next = e; // ⭐尾插法
hiTail = e;
}
} while ((e = next) != null);
优势:
-
保持链表原始顺序(A→B→C搬后仍是A→B→C)
-
多线程扩容时不会形成环(但仍有数据覆盖风险)
🛡️ 四、终极解决方案:告别死循环的四种武器
| 方法 | 原理 | 代码示例 |
|---|---|---|
| 1. 升级JDK 1.8+ | 尾插法避免死循环(但仍非线程安全) | Map<K,V> map = new HashMap<>(); |
| 2. 使用ConcurrentHashMap | 分段锁/CAS保证原子操作 | Map<K,V> safeMap = new ConcurrentHashMap<>();26 |
| 3. 外部加锁 | 手动控制并发访问 | synchronized(map) { <br> map.put(key, value);<br>} |
| 4. 只读视图 | 防止意外修改 | Map<K,V> readOnlyMap = Collections.unmodifiableMap(map); |
💎 工程建议:
高并发场景:必用
ConcurrentHashMap(JDK 1.8采用CAS+synchronized优化)历史系统:确保JDK ≥1.8并避免多线程写HashMap
面试要点:能画图解释JDK 1.7头插法成环过程
💎 总结:一图掌握HashMap死循环本质
面试口诀:
头插反转埋祸根,多线程扩容乱乾坤。
链表成环CPU爆,尾插并发定乾坤。
理解后试试在JDK 1.7环境运行复现代码(需配置旧JDK),亲眼见证死循环的产生。记住:多线程写HashMap永远是不定时炸弹,ConcurrentHashMap才是王道!