在單執行緒程式中,我們習慣使用 try...catch 來捕獲運行時異常。然而,當程式進入多執行緒領域,這套機制卻失效了。主執行緒的 try...catch 區塊無法捕獲子執行緒內部拋出的異常,導致異常被吞噬,只能在控制台打印堆疊追蹤,程式卻繼續運行,造成難以排查的隱患。
Thread 類的 run() 方法簽名不拋出任何受檢異常(Checked Exception)。當子執行緒因未捕獲的異常(如 ArithmeticException)終止時,該異常只會在該執行緒的上下文中傳播,最終由 JVM 的預設異常處理器輸出到錯誤流,主執行緒完全感知不到。
public class NoCaughtThread implements Runnable {
@Override
public void run() {
System.out.println(3 / 2);
System.out.println(3 / 0); // 拋出 ArithmeticException
System.out.println(3 / 1); // 永遠不會執行
}
public static void main(String[] args) {
try {
Thread thread = new Thread(new NoCaughtThread());
thread.start();
} catch (Exception e) {
// 永遠捕獲不到子執行緒的異常!
System.out.println("==Exception: " + e.getMessage());
}
}
}
運行結果:
1 Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero at threadtest.NoCaughtThread.run(NoCaughtThread.java:7) at java.lang.Thread.run(Thread.java:724)
最直接的方式是在 run() 方法內部包裹完整的 try...catch...finally,主動處理異常,並透過回調或共享物件將異常資訊傳遞給主執行緒。
public class InitiativeCaught {
public static void main(String[] args) {
InitialtiveThread initialtiveThread = new InitialtiveThread();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(initialtiveThread);
exec.shutdown();
}
}
class InitialtiveThread implements Runnable {
@Override
public void run() {
Throwable thrown = null;
try {
System.out.println(3 / 2);
System.out.println(3 / 0);
System.out.println(3 / 1);
} catch (Throwable e) {
thrown = e;
} finally {
threadDeal(this, thrown); // 統一處理邏輯
}
}
public void threadDeal(Runnable r, Throwable t) {
System.out.println("==Exception: " + t.getMessage());
}
}
優點:邏輯直觀,完全掌控異常處理流程。
缺點:侵入性強,每個 Runnable 都要重複編寫樣板代碼,難以統一管理。
Java API 提供了 Thread.UncaughtExceptionHandler 介面,專門用於捕獲執行緒因未捕獲異常而終止的情況。這是處理執行緒異常的標準、優雅方式。
public static interface Thread.UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
Thread.setDefaultUncaughtExceptionHandler(handler),對所有未單獨設置處理器的執行緒生效。thread.setUncaughtExceptionHandler(handler),精確控制特定執行緒的異常行為。public class ThreadMonitor implements Runnable {
private int data;
private int control = 0;
private static final int MAX_RETRY = 3;
public ThreadMonitor(int data) { this.data = data; }
@Override
public void run() {
// 為當前執行緒設置專屬異常處理器
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
System.out.println("==Exception: " + e.getMessage());
String msg = e.getMessage();
if (control >= MAX_RETRY || "ok".equals(msg)) {
return; // 達到上限或正常結束,不再重試
}
if ("error".equals(msg)) {
new Thread(() -> {
try {
System.out.println("開始睡眠 1 秒...");
Thread.sleep(1000);
control++;
System.out.println("睡眠結束,重試次數: " + control);
myTask(data); // 重新執行任務
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}).start();
}
});
myTask(data); // 首次執行
}
@SuppressWarnings("finally")
private void myTask(int data) {
boolean flag = true;
try {
System.out.println(4 / data); // data=0 時拋出 ArithmeticException
} catch (Exception e) {
flag = false;
} finally {
// 拋出帶標記的 RuntimeException,供 Handler 判斷
throw new RuntimeException(flag ? "ok" : "error");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ThreadMonitor(0)); // 傳入 0 觸發除零異常
exec.shutdown();
}
}
運行結果:
==Exception: error 開始睡眠 1 秒... 睡眠結束,重試次數: 1 ==Exception: error 開始睡眠 1 秒... 睡眠結束,重試次數: 2 ==Exception: error 開始睡眠 1 秒... 睡眠結束,重試次數: 3 ==Exception: error
UncaughtExceptionHandler 必須在執行緒啟動前(或 run() 方法開頭)設置,否則無法捕獲啟動過程中的異常。ThreadPoolExecutor 時,建議覆寫 afterExecute 方法或提交 Future 並調用 get() 來捕獲異常,因為線程池的工作線程會被復用,預設異常處理器行為可能不符合預期。Java 多執行緒異常監控的核心在於理解「異常不跨執行緒傳播」這一機制。拋棄試圖在主執行緒 catch 子執行緒異常的幻想,轉而擁抱 UncaughtExceptionHandler。它不僅解決了異常捕獲問題,更為實現重試、熔斷、告警等高級容錯模式提供了標準擴展點。在生產環境中,建議結合日誌框架(如 SLF4J)在 Handler 中記錄完整堆疊,並接入監控系統(如 Prometheus、SkyWalking)實現全鏈路異常可觀測。