💀 死法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:死鎖(互相拿鎖不放手)

經典四要素:

  1. 互斥條件(鎖不可共享)
  2. 佔有且等待
  3. 不可剝奪
  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(() -> {
    // 優雅關閉線程池、刷盤等
}));