💀 死法1:自然退休(最理想的結局)
✅ 線程執行完 run() 方法,自動進入 TERMINATED。
這是唯一不需要你操心的“善終”。
new Thread(() -> {
System.out.println("任務完成,光榮退休!");
}).start();
🔍 面試延伸問:
Q:主線程結束後,子線程還會繼續跑嗎?
A:會!只要還有非守護線程活着,JVM就不會退出。
💀 死法2:被 interrupt() 禮貌勸退(但很多人不會接招)
interrupt() 不是強制殺死線程,而是設置中斷標誌位。
線程必須自己檢查並響應!
場景1:正常輪詢中斷
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 模擬工作
doSomeHeavyTask();
}
System.out.println("收到中斷,主動退出");
});
worker.start();
// 5秒後中斷
Thread.sleep(5000);
worker.interrupt();
場景2:阻塞中被中斷 → 拋出 InterruptedException
try {
Thread.sleep(10000); // 這裏會被打斷
} catch (InterruptedException e) {
// ⚠️ 關鍵:catch後中斷狀態會被清除!
// 如果你想繼續傳遞中斷,要手動重置:
Thread.currentThread().interrupt();
}
📌 面試高頻陷阱:
很多同學 catch 了 InterruptedException 就完了,結果中斷信號丟失,線程無法退出!
✅ 正確做法:要麼退出循環,要麼重新設置中斷標誌。
💀 死法3:無限等待(等一個永遠不會來的 notify)
典型代碼:
synchronized (lock) {
lock.wait(); // 永遠沒人調 lock.notify()
}
線程狀態變成 WAITING (on object monitor),永遠卡住。
真實場景:
- 消費者啓動了,但生產者掛了,沒發通知
- 多個消費者,但 notify() 只喚醒一個,其他繼續等
🔧 解決方案:
- **永遠不要無條件 wait()!**加超時或判斷條件:
while (!conditionMet) {
lock.wait(5000); // 最多等5秒
}
- 用
Condition.await()+signalAll()更可控(配合 ReentrantLock)
🛠️ 排查工具:
用 jstack <pid> 查看線程堆棧,看到大量 WAITING 就要警惕!
💀 死法4:死鎖(互相拿鎖不放手)
經典四要素:
- 互斥條件(鎖不可共享)
- 佔有且等待
- 不可剝奪
- 循環等待
復現代碼:
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(() -> {
synchronized (lock1) {
System.out.println("T1 拿到 lock1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) { /* 想拿lock2 */ }
}
}).start();
new Thread(() -> {
synchronized (lock2) {
System.out.println("T2 拿到 lock2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) { /* 想拿lock1 */ }
}
}).start();
💥 結果:兩個線程互相卡住,CPU不飆,但程序不動!
🔍 如何發現死鎖?
jstack <pid>會直接提示:Found one Java-level deadlock- VisualVM / JConsole 圖形化查看線程依賴
✅ 預防策略:
- 所有線程按相同順序加鎖(比如先 lock1 再 lock2)
- 使用
tryLock(timeout)超時放棄 - 用
java.util.concurrent工具類(如 ConcurrentHashMap、StampedLock)
💀 死法5:活鎖(忙得飛起,就是幹不成)
和死鎖相反:線程沒阻塞,但一直在“讓步”或“重試”,無法推進。
類比:
兩個人在窄走廊相遇,都往左讓 → 撞上;
又都往右讓 → 又撞上……無限循環😅
代碼場景:
- 兩個線程同時嘗試 CAS 修改同一變量失敗,不斷重試
- 消息隊列消費者頻繁回滾,導致消息反覆消費失敗
✅ 解決思路:
- 引入隨機退避(Random Backoff)
- 設置最大重試次數
- 用鎖代替樂觀併發(犧牲性能換確定性)
💀 死法6:資源耗盡(社會性死亡)
線程本身沒死,但系統已經廢了。
常見表現:
- OOM:unable to create native thread
→ 線程太多,超過系統限制(ulimit -u) - CPU 100%
→ 死循環 or 忙等待(while(true) 不加 sleep) - Full GC 頻繁
→ 線程局部變量太大 or ThreadLocal 泄漏
🔥 ThreadLocal 泄漏經典坑:
private static ThreadLocal<List<String>> local = new ThreadLocal<>();
public void handle() {
local.set(new ArrayList<>()); // 用完沒 remove!
// 如果是線程池,線程複用 → 內存泄漏!
}
✅ 正確姿勢:
try {
local.set(...);
// 業務邏輯
} finally {
local.remove(); // 必須清理!
}
🛠️ 監控建議:
- 用
top -H -p <pid>看線程CPU佔用 - Prometheus + Grafana 監控線程池活躍數
- Arthas 的
thread命令實時查線程狀態
💀 死法7:被守護線程“連坐”陪葬
JVM規則:當所有非守護線程結束,JVM立即退出,不管守護線程在幹嘛!
Thread daemon = new Thread(() -> {
try {
while (true) {
writeLogToFile(); // 寫日誌到磁盤
Thread.sleep(1000);
}
} finally {
System.out.println("這行永遠不會執行!");
}
});
daemon.setDaemon(true);
daemon.start();
// 主線程睡2秒後結束
Thread.sleep(2000);
System.out.println("Main exit");
💥 結果:daemon線程直接被殺,日誌可能丟失,finally不執行!
✅ 最佳實踐:
- 守護線程只做非關鍵後台任務(如心跳、監控)
- 關鍵任務(寫文件、發消息)必須用非守護線程 or 優雅關閉鈎子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 優雅關閉線程池、刷盤等
}));