java 線程池的異常處理機(jī)制

一、前言

線程池技術(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ī)制。

二、Thread

在談線程池的異常處理之前,我們先來看 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);

? ? ? ? }

? ? }

}

三、ThreadPoolExecutor

在 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語(yǔ)句;或者使用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 工具類庫(kù)都為我們提供了相應(yīng)的類庫(kù)以簡(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顺?。雖然線程池會(huì)創(chuàng)建一個(gè)新的工作線程,但是如果這個(gè)步驟反復(fù)執(zhí)行,效率自然會(huì)下降很多。

四、ScheduledThreadPoolExecutor

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ò)了卻毫不知情。

五、結(jié)論

在Thread中,我們可以通過UncaughtExceptionHandler來實(shí)現(xiàn)默認(rèn)的異常處理機(jī)制。但是在使用ThreadPoolExecutor和ScheduledThreadPoolExecutor這兩個(gè) JDK 最主要的線程池時(shí),使用UncaughtExceptionHandler是不合適的。所以,try...catch往往是不可避免的,否則你的任務(wù)很有可能失敗的悄無聲息。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、前言 線程池技術(shù)是服務(wù)器端開發(fā)中常用的技術(shù)。不論是直接還是間接,各種服務(wù)器端功能的執(zhí)行總是離不開線程池的調(diào)度。...
    編走編想閱讀 5,631評(píng)論 4 10
  • 今天小伙伴遇到個(gè)小問題,線程池提交的任務(wù)如果沒有catch異常,那么會(huì)拋到哪里去,之前倒是沒研究過,本著實(shí)事求是的...
    java菜閱讀 1,051評(píng)論 0 1
  • 第一部分 來看一下線程池的框架圖,如下: 1、Executor任務(wù)提交接口與Executors工具類 Execut...
    壓抑的內(nèi)心閱讀 4,395評(píng)論 1 24
  • 文/ 陳皓 寒冷的空氣里飄著飛舞的雪花 我的靈魂在無垠的陣地上游走 巡邏放哨是我的最愛,這樣 我會(huì)離和平的國(guó)界碑更...
    沂蒙文學(xué)閱讀 315評(píng)論 6 5
  • 新一代ipfs+dtp區(qū)塊鏈分布式存儲(chǔ)礦機(jī) 現(xiàn)在面向全國(guó)招商,招代理。 多項(xiàng)專利,多鏈并存,一機(jī)多挖, 礦機(jī)單臺(tái)收...
    6c24d81ff862閱讀 178評(píng)論 0 0

友情鏈接更多精彩內(nèi)容