一、前言
線程池技術(shù)是服務(wù)器端開發(fā)中常用的技術(shù)偏灿。不論是直接還是間接钝的,各種服務(wù)器端功能的執(zhí)行總是離不開線程池的調(diào)度铆遭。關(guān)于線程池的各種文章,多數(shù)是關(guān)注任務(wù)的創(chuàng)建和執(zhí)行方面碗脊,對于異常處理和任務(wù)取消(包括線程池關(guān)閉)關(guān)注的偏少橄妆。
接下來,本文將從 Java 原生線程矢劲、兩種主要線程池 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
這三方面介紹 Java 中線程的異常處理機制慌随。
二、Thread
在談線程池的異常處理之前丸逸,我們先來看 Java 中線程中的異常是如何被處理的剃袍。大家都知道如何創(chuàng)建一個線程任務(wù):
代碼1
Thread t = new Thread(() -> System.out.println("Execute in a thread"));
t.start();
為了簡化代碼,這里使用了 Java 8 的 Lambda 表達式憔维。() -> System.out.println("Execute in a thread")
等同于在 Runnable
中執(zhí)行 System.out.println
方法研铆。后面不再解釋。
如果這個任務(wù)拋出了異常凶赁,那又會怎樣:
代碼2
Thread t = new Thread(() -> System.out.println(1 / 0));
t.start();
如果我們執(zhí)行上面這段代碼,會在控制臺上看到異常輸出致板∮搅可能多數(shù)同學(xué)會對此不會覺得問題,但是問題在于萝挤,通常情況下絕大多數(shù)線上應(yīng)用不會將控制臺作為日志輸出地址根欧,而是另有日志輸出。這種情況下酥泛,上面的代碼所拋出異常便會丟失嫌拣。
那為了將異常輸出到日志中,我們會這樣寫代碼:
代碼3
Thread t = new Thread(() -> {
try {
System.out.println(1 / 0);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
});
t.start();
這樣我們就能異常棧輸出到日志中捶索,而不是控制臺应役,從而避免異常的丟失。
過了一段時間院崇,問題又來了袍祖,可能好多線程任務(wù)默認的異常處理機制都是相同的。比如都是將異常輸出到日志文件捐凭。按照上面的寫法會造成重復(fù)代碼凳鬓。雖然重復(fù)的不多,但是有代碼潔癖的小伙伴可能也會覺得不舒服垦梆。
那我們該如何解決這個問題呢?其實 JDK 已經(jīng)為我們想到了印蓖,Thread
類中有個接口 UncaughtExceptionHandler
京腥。通過實現(xiàn)這個接口,并調(diào)用 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)
方法公浪,我們就能為一個線程設(shè)置默認的異常處理機制因悲,避免重復(fù)的 try...catch
了勺爱。
除此以外,我們還可以通過 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)
設(shè)置全局的默認異常處理機制卫旱。此外围段,ThreadGroup
也實現(xiàn)了 UncaughtExceptionHandler
接口,所以通過 ThreadGroup
還可以為一組線程設(shè)置默認的異常處理機制适贸。
其實涝桅,之所以代碼2在執(zhí)行之后我們能在控制臺上看到異常,也是因為 UncaughtExceptionHandler
機制蕊肥。ThreadGroup
默認提供了異常處理機制如下:
代碼4
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
// 最終執(zhí)行如下代碼
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
三蛤肌、ThreadPoolExecutor
在 Java 5 發(fā)布之后裸准,線程池便開始越來越廣泛地用于創(chuàng)建并發(fā)任務(wù)。多數(shù)時候炒俱,當(dāng)說到 Java 的線程池時,我們一般指的就是 ThreadPoolExecutor
恼蓬。那在 ThreadPoolExecutor
中是如何處理異常的呢?
代碼5
Executors.newSingleThreadExecutor().execute(() -> {
throw new RuntimeException("My runtime exception");
});
上面的代碼的異常處理機制其實同直接使用 Thread
是一樣的小槐。所以也有同樣的問題荷辕,異常信息無法反映在日志文件中疮方。解決這個問題的方法同上一節(jié)一樣:在每個 Runnable
中編寫 try ... catch
語句;或者使用 UncaughtExceptionHandler
機制骡显。
我們先來看如何為線程池中的工作線程設(shè)置 UncaughtExceptionHandler
惫谤。
為線程池工作線程設(shè)置 UncaughtExceptionHandler
簡單來說,就是通過 ThreadFactory
溜歪。通過 ThreadPoolExecutor
的構(gòu)造函數(shù)和 Executors
中的工具方法蝴猪,我們都可以為新創(chuàng)建的線程池設(shè)置 ThreadFactory
。
ThreadFactory
是個接口嚎莉,它只定義了一個方法 Thread newThread(Runnable r)
动壤。在這個方法中,我們可以為新創(chuàng)建出來的線程設(shè)置 UncaughtExceptionHandler
阁簸。當(dāng)然哼丈,這樣寫起來顯得很麻煩,好在 Apache Commons 和 Google Guava 這兩個最有名的 Java 工具類庫都為我們提供了相應(yīng)的類庫以簡化配置 ThreadFactory
的工作饶米。下面以 Apache Commons 提供的 BasicThreadFactoryBuilder
為例
代碼6
ThreadFactory executorThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("task-scanner-executor-%d")
.uncaughtExceptionHandler(new LogUncaughtExceptionHandler(LOGGER))
.build();
Executors.newSingleThreadExecutor(executorThreadFactory);
UncaughtExceptionHandler 一定起作用嗎?
此話怎講呢照瘾?其實 ThreadPoolExecutor
為執(zhí)行并發(fā)任務(wù)提供了兩種方法:execute(Runnable)
和 submit(Callable/Runnable)
丧慈。之前的代碼示例只演示了執(zhí)行 execute(Runnable)
時的情況。那在設(shè)置了默認的 UncaughtExceptionHandler
之后鹃愤,當(dāng)執(zhí)行 submit(Callable/Runnable)
方法完域,拋出拋異常之后有會如何?
看下面的代碼
代碼7
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(new LogExceptionHandler())
.build();
Executors.newSingleThreadExecutor(threadFactory)
.submit(() -> {
throw new RuntimeException("test");
});
上面的程序執(zhí)行完之后凹耙,不會在控制臺或日志中看到任何輸出乌妙,雖然設(shè)置了 UncaughtExceptionHandler
藤韵。要弄清原因熊经,就要看一下 ThreadPoolExecutor
的源代碼
代碼8
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit
方法是調(diào)用 execute
實現(xiàn)任務(wù)執(zhí)行的。但是在調(diào)用 execute
之前匹涮,任務(wù)會被封裝進 FutureTask
類中槐壳,然后最終工作線程執(zhí)行的是 FutureTask
中的 run
方法。
代碼9:FutureTask.run
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
由上面的代碼可以看出雳攘,不同于直接調(diào)用 execute
方法枫笛,調(diào)用 submit
方法后刑巧,如果任務(wù)拋出異常无畔,會被 setException
方法賦給代表執(zhí)行結(jié)果的 outcome
變量吠冤,而不會繼續(xù)拋出。因此闸昨,UncaughtExceptionHandler
也沒有機會處理薄风。
如果想知道 submit
的執(zhí)行結(jié)果是成功還是失敗,必須調(diào)用 Future.get()
方法循诉。
UncaughtExceptionHandler 是否適合在線程池中使用
從上面的分析中可以看出撇他,使用 UncaughtExceptionHandler
,可以處理到使用 execute
方法執(zhí)行任務(wù)所拋出的異常划纽,但是對 submit
方法無效锌畸。那如果只是用 execute
方法潭枣,我們是否可以通過設(shè)置 UncaughtExceptionHandler
從而添加一種默認的異常處理機制,以避免重復(fù)的 try...catch
代碼呢盆犁?
答案是不能谐岁。原因在于,如果在執(zhí)行 execute
方法時不在 Runnable.run
方法中寫 try...catch
方法伊佃,自然異常會交由 UncaughtExceptionHandler
處理锭魔,但是,在這之前织咧,線程的工作線程會因為異常而退出。雖然線程池會創(chuàng)建一個新的工作線程笙蒙,但是如果這個步驟反復(fù)執(zhí)行,效率自然會下降很多捅位。
四、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
是另一種常用的線程池尿扯,常用了執(zhí)行延遲任務(wù)或定時任務(wù)焰雕。常用的方法為 scheduleXXX
系列矩屁。那在這個線程池中異常是如何處理的呢?
其實泊脐,如果看過前面的部分烁峭,到這里也基本能猜出來了。ScheduledThreadPoolExecutor
用來封裝任務(wù)的是 ScheduledFutureTask
耘柱。ScheduledFutureTask
是 FutureTask
的子類棍现,所以镜遣,異常也會被復(fù)制給 outcome
。
但是谎僻,這里還是有一些差異的寓辱。在使用 ThreadPoolExecutor.submit
和 ScheduledThreadPoolExecutor.schedule
方法時秫筏,我們可以通過這兩個方法返回的 Future
來獲得執(zhí)行結(jié)果挎挖,這包括正常結(jié)果航夺,也包括異常結(jié)果。但是始衅,對于 ScheduledThreadPoolExecutor.scheduleWithFixedDelay
和 scheduleAtFixedRate
這兩個方法缭保,其返回的 Future
只會用來取消任務(wù),而不是得到結(jié)果诸老。原因也很容易理解彻亲,因為這兩個方法執(zhí)行的是定時任務(wù),是反復(fù)執(zhí)行的畸肆。這也是為什么這兩個方法的任務(wù)定義使用了 Runnable
接口宙址,而不是有返回值的 Callable
接口抡砂。因此,對于這兩個方法來說注益,在 Runnable.run
方法中加 try...catch
是必須的丑搔,否則很有可能出錯了卻毫不知情。
五煮仇、結(jié)論
在 Thread
中谎仲,我們可以通過 UncaughtExceptionHandler
來實現(xiàn)默認的異常處理機制。但是在使用 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
這兩個 JDK 最主要的線程池時夹姥,使用 UncaughtExceptionHandler
是不合適的。所以佃声,try...catch
往往是不可避免的圾亏,否則你的任務(wù)很有可能失敗的悄無聲息。