一灼狰、前言
線程池技術(shù)是服務(wù)器端開發(fā)中常用的技術(shù)掩幢。不論是直接還是間接或油,各種服務(wù)器端功能的執(zhí)行總是離不開線程池的調(diào)度寞忿。關(guān)于線程池的各種文章,多數(shù)是關(guān)注任務(wù)的創(chuàng)建和執(zhí)行方面顶岸,對(duì)于異常處理和任務(wù)取消(包括線程池關(guān)閉)關(guān)注的偏少腔彰。
接下來,本文將從 Java 原生線程辖佣、兩種主要線程池ThreadPoolExecutor和ScheduledThreadPoolExecutor這三方面介紹 Java 中線程的異常處理機(jī)制霹抛。
在談線程池的異常處理之前凌简,我們先來看 Java 中線程中的異常是如何被處理的。大家都知道如何創(chuàng)建一個(gè)線程任務(wù):
代碼1
Thread t =newThread(() -> System.out.println("Execute in a thread"));
t.start();
為了簡(jiǎn)化代碼恃逻,這里使用了 Java 8 的 Lambda 表達(dá)式雏搂。() -> System.out.println("Execute in a thread")等同于在Runnable中執(zhí)行System.out.println方法。后面不再解釋寇损。
如果這個(gè)任務(wù)拋出了異常凸郑,那又會(huì)怎樣:
代碼2
Thread t =newThread(() -> System.out.println(1/0));
t.start();
如果我們執(zhí)行上面這段代碼,會(huì)在控制臺(tái)上看到異常輸出矛市≤搅ぃ可能多數(shù)同學(xué)會(huì)對(duì)此不會(huì)覺得問題,但是問題在于浊吏,通常情況下絕大多數(shù)線上應(yīng)用不會(huì)將控制臺(tái)作為日志輸出地址而昨,而是另有日志輸出。這種情況下找田,上面的代碼所拋出異常便會(huì)丟失歌憨。
那為了將異常輸出到日志中,我們會(huì)這樣寫代碼:
代碼3
Thread t =newThread(() -> {
try{
System.out.println(1/0);
}catch(Exceptione) {
? ? ? ? LOGGER.error(e.getMessage(), e);
? ? }
});
t.start();
這樣我們就能異常棧輸出到日志中墩衙,而不是控制臺(tái)务嫡,從而避免異常的丟失。
過了一段時(shí)間漆改,問題又來了心铃,可能好多線程任務(wù)默認(rèn)的異常處理機(jī)制都是相同的。比如都是將異常輸出到日志文件挫剑。按照上面的寫法會(huì)造成重復(fù)代碼去扣。雖然重復(fù)的不多,但是有代碼潔癖的小伙伴可能也會(huì)覺得不舒服樊破。
那我們?cè)撊绾谓鉀Q這個(gè)問題呢厅篓?其實(shí) JDK 已經(jīng)為我們想到了秀存,Thread類中有個(gè)接口UncaughtExceptionHandler。通過實(shí)現(xiàn)這個(gè)接口羽氮,并調(diào)用Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)方法或链,我們就能為一個(gè)線程設(shè)置默認(rèn)的異常處理機(jī)制,避免重復(fù)的try...catch了档押。
除此以外澳盐,我們還可以通過Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)設(shè)置全局的默認(rèn)異常處理機(jī)制。此外令宿,ThreadGroup也實(shí)現(xiàn)了UncaughtExceptionHandler接口叼耙,所以通過ThreadGroup還可以為一組線程設(shè)置默認(rèn)的異常處理機(jī)制。
其實(shí)粒没,之所以代碼2在執(zhí)行之后我們能在控制臺(tái)上看到異常筛婉,也是因?yàn)閁ncaughtExceptionHandler機(jī)制。ThreadGroup默認(rèn)提供了異常處理機(jī)制如下:
代碼4
publicvoiduncaughtException(Thread t, Throwable e){
if(parent!=null) {
parent.uncaughtException(t, e);
}else{
? ? ? ? Thread.UncaughtExceptionHandler ueh =
? ? ? ? ? ? Thread.getDefaultUncaughtExceptionHandler();
if(ueh !=null) {
? ? ? ? ? ? ueh.uncaughtException(t, e);
}elseif(!(einstanceofThreadDeath)) {
// 最終執(zhí)行如下代碼
System.err.print("Exception in thread \""
+ t.getName() +"\" ");
? ? ? ? ? ? e.printStackTrace(System.err);
? ? ? ? }
? ? }
}
在 Java 5 發(fā)布之后爽撒,線程池便開始越來越廣泛地用于創(chuàng)建并發(fā)任務(wù)。多數(shù)時(shí)候响蓉,當(dāng)說到 Java 的線程池時(shí)硕勿,我們一般指的就是ThreadPoolExecutor。那在ThreadPoolExecutor中是如何處理異常的呢枫甲?
代碼5
Executors.newSingleThreadExecutor().execute(() -> {
thrownewRuntimeException("My runtime exception");
});
上面的代碼的異常處理機(jī)制其實(shí)同直接使用Thread是一樣的源武。所以也有同樣的問題,異常信息無法反映在日志文件中想幻。解決這個(gè)問題的方法同上一節(jié)一樣:在每個(gè)Runnable中編寫try ... catch語句粱栖;或者使用UncaughtExceptionHandler機(jī)制。
我們先來看如何為線程池中的工作線程設(shè)置UncaughtExceptionHandler脏毯。
為線程池工作線程設(shè)置 UncaughtExceptionHandler
簡(jiǎn)單來說查排,就是通過ThreadFactory。通過ThreadPoolExecutor的構(gòu)造函數(shù)和Executors中的工具方法抄沮,我們都可以為新創(chuàng)建的線程池設(shè)置ThreadFactory跋核。
ThreadFactory是個(gè)接口,它只定義了一個(gè)方法Thread newThread(Runnable r)叛买。在這個(gè)方法中砂代,我們可以為新創(chuàng)建出來的線程設(shè)置UncaughtExceptionHandler。當(dāng)然率挣,這樣寫起來顯得很麻煩刻伊,好在 Apache Commons 和 Google Guava 這兩個(gè)最有名的 Java 工具類庫都為我們提供了相應(yīng)的類庫以簡(jiǎn)化配置ThreadFactory的工作。下面以 Apache Commons 提供的BasicThreadFactoryBuilder為例
代碼6
ThreadFactory executorThreadFactory =newBasicThreadFactory.Builder()
.namingPattern("task-scanner-executor-%d")
.uncaughtExceptionHandler(newLogUncaughtExceptionHandler(LOGGER))
? ? ? ? .build();
Executors.newSingleThreadExecutor(executorThreadFactory);
UncaughtExceptionHandler 一定起作用嗎?
此話怎講呢捶箱?其實(shí)ThreadPoolExecutor為執(zhí)行并發(fā)任務(wù)提供了兩種方法:execute(Runnable)和submit(Callable/Runnable)智什。之前的代碼示例只演示了執(zhí)行execute(Runnable)時(shí)的情況。那在設(shè)置了默認(rèn)的UncaughtExceptionHandler之后丁屎,當(dāng)執(zhí)行submit(Callable/Runnable)方法荠锭,拋出拋異常之后有會(huì)如何?
看下面的代碼
代碼7
ThreadFactory threadFactory =newThreadFactoryBuilder()
.setUncaughtExceptionHandler(newLogExceptionHandler())
? ? ? ? .build();
Executors.newSingleThreadExecutor(threadFactory)
? ? ? ? .submit(() -> {
thrownewRuntimeException("test");
? ? ? ? });
上面的程序執(zhí)行完之后晨川,不會(huì)在控制臺(tái)或日志中看到任何輸出证九,雖然設(shè)置了UncaughtExceptionHandler。要弄清原因共虑,就要看一下ThreadPoolExecutor的源代碼
代碼8
publicFuture submit(Runnabletask) {
if(task==null)thrownewNullPointerException();
RunnableFuture ftask = newTaskFor(task,null);
? ? execute(ftask);
returnftask;
}
submit方法是調(diào)用execute實(shí)現(xiàn)任務(wù)執(zhí)行的愧怜。但是在調(diào)用execute之前,任務(wù)會(huì)被封裝進(jìn)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ù)拋出異常,會(huì)被setException方法賦給代表執(zhí)行結(jié)果的outcome變量音诫,而不會(huì)繼續(xù)拋出惨奕。因此雪位,UncaughtExceptionHandler也沒有機(jī)會(huì)處理竭钝。
如果想知道submit的執(zhí)行結(jié)果是成功還是失敗,必須調(diào)用Future.get()方法雹洗。
UncaughtExceptionHandler 是否適合在線程池中使用
從上面的分析中可以看出香罐,使用UncaughtExceptionHandler,可以處理到使用execute方法執(zhí)行任務(wù)所拋出的異常时肿,但是對(duì)submit方法無效庇茫。那如果只是用execute方法,我們是否可以通過設(shè)置UncaughtExceptionHandler從而添加一種默認(rèn)的異常處理機(jī)制螃成,以避免重復(fù)的try...catch代碼呢旦签?
答案是不能。原因在于寸宏,如果在執(zhí)行execute方法時(shí)不在Runnable.run方法中寫try...catch方法宁炫,自然異常會(huì)交由UncaughtExceptionHandler處理,但是氮凝,在這之前羔巢,線程的工作線程會(huì)因?yàn)楫惓6顺觥km然線程池會(huì)創(chuàng)建一個(gè)新的工作線程,但是如果這個(gè)步驟反復(fù)執(zhí)行竿秆,效率自然會(huì)下降很多启摄。
ScheduledThreadPoolExecutor是另一種常用的線程池幽钢,常用了執(zhí)行延遲任務(wù)或定時(shí)任務(wù)歉备。常用的方法為scheduleXXX系列。那在這個(gè)線程池中異常是如何處理的呢搅吁?
其實(shí)威创,如果看過前面的部分,到這里也基本能猜出來了谎懦。ScheduledThreadPoolExecutor用來封裝任務(wù)的是ScheduledFutureTask肚豺。ScheduledFutureTask是FutureTask的子類,所以界拦,異常也會(huì)被復(fù)制給outcome吸申。
但是,這里還是有一些差異的享甸。在使用ThreadPoolExecutor.submit和ScheduledThreadPoolExecutor.schedule方法時(shí)截碴,我們可以通過這兩個(gè)方法返回的Future來獲得執(zhí)行結(jié)果,這包括正常結(jié)果蛉威,也包括異常結(jié)果日丹。但是,對(duì)于ScheduledThreadPoolExecutor.scheduleWithFixedDelay和scheduleAtFixedRate這兩個(gè)方法蚯嫌,其返回的Future只會(huì)用來取消任務(wù)哲虾,而不是得到結(jié)果。原因也很容易理解择示,因?yàn)檫@兩個(gè)方法執(zhí)行的是定時(shí)任務(wù)束凑,是反復(fù)執(zhí)行的。這也是為什么這兩個(gè)方法的任務(wù)定義使用了Runnable接口栅盲,而不是有返回值的Callable接口汪诉。因此,對(duì)于這兩個(gè)方法來說谈秫,在Runnable.run方法中加try...catch是必須的扒寄,否則很有可能出錯(cuò)了卻毫不知情。
在Thread中该编,我們可以通過UncaughtExceptionHandler來實(shí)現(xiàn)默認(rèn)的異常處理機(jī)制。但是在使用ThreadPoolExecutor和ScheduledThreadPoolExecutor這兩個(gè) JDK 最主要的線程池時(shí)构灸,使用UncaughtExceptionHandler是不合適的上渴。所以岸梨,try...catch往往是不可避免的,否則你的任務(wù)很有可能失敗的悄無聲息稠氮。