在 Java 開發中,異常處理是保障程式穩定性的關鍵環節。然而,許多開發者發現 try-catch 在單線程中運作良好,一旦進入多線程環境卻失去作用。本文深入探討線程異常無法被主線程捕獲的根本原因,並提供兩種主流解決方案:run() 內部自行處理與 Thread.UncaughtExceptionHandler 接口,並附帶完整代碼示例與運行結果分析。
try-catch 失效?在單線程程式中,異常會沿著調用棧向上傳播,最終被 try-catch 捕獲。但多線程時,每個線程擁有獨立的調用棧。主線程啟動子線程後隨即繼續執行,子線程拋出的異常只會在自己的棧中傳播,無法跨越線程邊界到達主線程的 catch 塊。
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)
異常直接打印到控制台,導致線程悄無聲息地終止,主線程毫不知情。根本原因在於 Thread.run() 方法簽名不拋出受檢異常,且 JVM 不會自動將非受檢異常傳遞給父線程。
run() 內部主動捕獲並處理最直觀的方式是在 run() 方法頂層包裹 try-catch(Throwable),捕獲所有異常並委派給自定義處理邏輯。此法適用於簡單場景,但需在每個 Runnable 中重複編寫樣板代碼。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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());
}
}
運行結果:
1
==Exception: / by zero
Thread.UncaughtExceptionHandler(推薦)Java 5 引入 Thread.UncaughtExceptionHandler 接口,專門用於捕獲線程因未捕獲異常而終止的情況。它提供了兩種設置方式:
Thread.setDefaultUncaughtExceptionHandler(handler),對所有未顯式設置處理器的線程生效。thread.setUncaughtExceptionHandler(handler),精確控制某線程的異常行為。下方示例演示「異常發生時重試最多 3 次」的業務邏輯:
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadMonitor implements Runnable {
private int data;
private int control = 0;
private static final int MAX = 3;
public ThreadMonitor(int i) { this.data = i; }
@Override
public void run() {
// 必須在 run() 內部設置,否則無法捕獲本線程異常
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("==Exception: " + e.getMessage());
String msg = e.getMessage();
if (control == MAX) return;
else if ("ok".equals(msg)) return;
else if ("error".equals(msg)) {
new Thread(() -> {
try {
System.out.println("開始睡眠。");
Thread.sleep(1000);
control++;
System.out.println("睡眠完畢,control: " + control);
myTask(data);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}).start();
}
}
});
myTask(data);
}
@SuppressWarnings("finally")
public void myTask(int data) {
boolean flag = true;
try {
System.out.println(4 / data); // data=0 觸發除零異常
} catch (Exception e) {
flag = false;
} finally {
if (flag) throw new RuntimeException("ok");
else throw new RuntimeException("error");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ThreadMonitor monitor = new ThreadMonitor(0);
exec.execute(monitor);
exec.shutdown();
}
}
運行結果:
==Exception: error
開始睡眠。
睡眠完畢,control: 1
==Exception: error
開始睡眠。
睡眠完畢,control: 2
==Exception: error
開始睡眠。
睡眠完畢,control: 3
==Exception: error
UncaughtExceptionHandler 接口定義:public void uncaughtException(Thread t, Throwable e)。run() 方法內設置處理器(或線程啟動前調用 setUncaughtExceptionHandler),否則無法捕獲該線程異常。ThreadFactory 在創建線程時統一注入處理器,避免遺漏。