前言
分析線程對異常的處理機(jī)制避归,首先要了解Java自身的異常處理機(jī)制,關(guān)于 try笨奠、catch袭蝗、finally、throw般婆、throws 這些關(guān)鍵字及 未檢查異常 和 檢查異常 等基礎(chǔ)不進(jìn)行展開到腥,本文主要討論對異常的處理。
未捕獲異常去哪里了蔚袍?
一個異常被拋出后左电,如果沒有被捕獲處理,那么會一直向上拋出页响,直到被main()/Thread.run()拋出。異常被main()/Thread.run() 拋出后段誊,會由虛擬機(jī)調(diào)用Thread 的 dispatchUncaughtException方法:
/**
* 向 handler 分派未捕獲的異常闰蚕。 該方法僅由JVM調(diào)用。
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
// 獲取未捕獲異常處理的 handler连舍,如果沒有設(shè)置則返回當(dāng)前線程所屬的ThreadGroup
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
ThreadGroup繼承自 UncaughtExceptionHandler没陡,實現(xiàn)了默認(rèn)的處理方法:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) { // 父級優(yōu)先處理
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) { // 沒有配置handler時的處理方式
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
當(dāng)有異常被main()/Thread.run() 拋出后,JVM會調(diào)用 dispatchUncaughtException
來尋找handler進(jìn)行處理。而如果沒有自定義hanlder進(jìn)行處理盼玄,則會調(diào)用 System.err 進(jìn)行輸出贴彼。
線程對異常的處理
本文將線程實現(xiàn)分為兩種,無返回值的Runnable實現(xiàn) 和 有返回值的FutureTask實現(xiàn)埃儿。然后分為 Thread 器仗、ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 三種情況來討論異常的處理。
一童番、Thread 異常處理
Thread 線程的有很多種實現(xiàn)形式精钮,基于 Runnable 的沒有返回值的線程,無法在主線程感知到異常剃斧,沒有捕獲的異常只會輸出到控制臺轨香,如果沒有捕獲異常,進(jìn)行處理或日志記錄幼东,會造成異常丟失臂容。有返回值的 FutureTask ,未捕獲的異常根蟹,會在獲取返回值時傳遞給主線程脓杉。
1. 主線程無法捕獲子線程異常
代碼一:
public static void catchInMainThread(){
try {
Thread thread = new Thread(() -> {
System.out.println( 1 / 0);
});
thread.start();
} catch (Exception e1) {
System.out.println("catched exception in main thread");
}
}
// 代碼一輸出
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at ThreadExecptionTest.lambda$0(ThreadExecptionTest.java:14)
at java.lang.Thread.run(Thread.java:745)
上述代碼無法捕獲到異常,根據(jù)開篇介紹的默認(rèn)處理機(jī)制娜亿,異常在子線程的Thread.run()方法中拋出后丽已,JVM會調(diào)用 dispatchUncaughtException
方法最終輸出到控制臺。
2. 子線程處理Runnable異常
代碼二 :
public static void catchInRunThread() {
Thread thread = new Thread(() -> {
try {
System.out.println(1 / 0);
} catch (Exception e1) {
// 異常處理买决,日志記錄等
System.out.println("catched exception in child thread");
}
});
thread.start();
}
// 代碼二輸出結(jié)果
catched exception in child thread
代碼二展示了線程內(nèi)按需處理異常的方式沛婴,這是一種推薦的線程內(nèi)異常處理方法。
3. 主線程處理FutureTask異常
public static void catchInFuture() {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
return 1/0;
});
Thread thread = new Thread(futureTask);
thread.start();
try {
futureTask.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("catched exception in main thread by future");
// e.printStackTrace();
}
}
輸出如下:
catched exception in main thread by future
FutureTask 的異常成功在主線程里處理督赤。
4. 配置默認(rèn)線程異常處理方式
代碼三:
public static void catchByExceptionHandler() {
Thread thread = new Thread(() -> {
System.out.println(1 / 0);
});
UncaughtExceptionHandler eh = new MyUncaughtExceptionHandler();
thread.setUncaughtExceptionHandler(eh);
thread.start();
}
static class MyUncaughtExceptionHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
// 異常處理嘁灯,日志記錄等
System.out.println("catched exception in ExceptionHandler");
}
}
// 代碼三輸出:
catched exception in ExceptionHandler
使用UncaughtExceptionHandler 來處理未捕獲異常也是一個可參考的方案。
二躲舌、ThreadPoolExecutor 異常處理
public class ThreadPoolExecutorExceptionTest {
public static void main(String[] args) throws InterruptedException, IOException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
executeThreadPoolExecutor(executor);
Thread.sleep(100);
submitThreadPoolExecutorWithOutGet(executor);
Thread.sleep(100);
submitThreadPoolExecutorAndGet(executor);
Thread.sleep(100);
executor.shutdown();
}
// Runnable子線程沒有捕獲異常
static void executeThreadPoolExecutor(ThreadPoolExecutor executor) {
System.out.println("\n*****executeThreadPoolExecutor*****");
executor.execute(() -> {
System.out.println(1 / 0);
});
}
// Callable不獲取返回值
static void submitThreadPoolExecutorWithOutGet(ThreadPoolExecutor executor) {
System.out.println("\n*****submitThreadPoolExecutorWithOutGet*****");
executor.submit(() -> {
System.out.println(1 / 0);
});
}
// Callable獲取返回值
static void submitThreadPoolExecutorAndGet(ThreadPoolExecutor executor) {
System.out.println("\n*****submitThreadPoolExecutorAndGet*****");
Future<?> future = executor.submit(() -> {
System.out.println(1 / 0);
});
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("catched exception in main thread by future");
// e.printStackTrace();
}
}
}
上面展示了三種ThreadPoolExecutor提交任務(wù)的形式丑婿,輸出如下:
*****executeThreadPoolExecutor*****
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at test.ThreadTest.lambda$1(ThreadTest.java:65)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
*****submitThreadPoolExecutorWithOutGet*****
*****submitThreadPoolExecutorAndGet*****
catched exception in main thread by future
直接用execute執(zhí)行Runnable方法,如果主線程里的異常未捕獲没卸,將被默認(rèn)handler輸出到控制臺羹奉,與Thread執(zhí)行任務(wù)類似。如果使用submit執(zhí)行FutureTask任務(wù)约计,而不調(diào)用get方法獲取返回值诀拭,則會造成異常丟失現(xiàn)象。使用FutureTask只有調(diào)用get方法才能感知到子線程是否正常執(zhí)行煤蚌。因此耕挨,為了確保任務(wù)順利執(zhí)行细卧,需使用get方法確認(rèn)返回結(jié)果是否有異常拋出。
三筒占、ScheduledThreadPoolExecutor 異常處理
之所以要將ScheduledThreadPoolExecutor 線程池單獨拿出來講贪庙,是因為ScheduledThreadPoolExecutor 的調(diào)度方法與普通線程池執(zhí)行方法不同,有著特殊的規(guī)則翰苫。
ScheduledThreadPoolExecutor 有個三個調(diào)度方法:schedule
止邮、scheduleAtFixedRate
、scheduleWithFixedDelay
革骨,schedule
方法有個重載方法农尖,可以傳Callable執(zhí)行,詳情可以參閱線程池之ScheduledThreadPoolExecutor概述良哲。
謹(jǐn)防 ScheduledThreadPoolExecutor 異常丟失
從上圖我們可以看到盛卡,這幾個方法都有返回值為 ScheduledFuture ,根據(jù)前面的分析我們知道有返回值的Future可以在獲取返回值是捕獲到異常筑凫,不獲取返回值就會造成異常丟失滑沧。我們來驗證一下:
public class ScheduledPoolExecptionTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
schedule(executor);
}
static void schedule(ScheduledThreadPoolExecutor executor) {
executor.schedule(() -> {
int n = 1/0; // 應(yīng)該拋出異常
}, 10, TimeUnit.NANOSECONDS);
}
}
用ScheduledThreadPoolExecutor的schedule方法執(zhí)行Runnable,上述代碼沒有任何輸出巍实,異常消失了滓技。這是平時我們使用調(diào)度線程池很容易犯的一個錯誤,這種異常消失在生產(chǎn)環(huán)境中將產(chǎn)生難以發(fā)現(xiàn)的問題棚潦。
ScheduledThreadPoolExecutor 的其他幾個調(diào)度方法也會產(chǎn)生類似的問題令漂。由于我們執(zhí)行的是Runnable任務(wù),往往不會調(diào)用future的get方法丸边,因此在開發(fā)中叠必,應(yīng)該多加小心這種異常吞并。避免產(chǎn)生此問題妹窖,盡量在線程內(nèi)捕獲異常并處理纬朝;
ScheduledThreadPoolExecutor 的execute方法和submit繼承自
ThreadPoolExecutor,異常處理機(jī)制一樣骄呼。
總結(jié)
不管是Thread 共苛、ThreadPoolExecutor 還是ScheduledThreadPoolExecutor,都應(yīng)秉承線程內(nèi)部處理的原則蜓萄,做到自己的事情自己辦隅茎。正確的處理、記錄異常嫉沽,及時的發(fā)現(xiàn)問題患膛,避免不必要的麻煩。