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語句粱栖;或者使用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

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)系作者
  • 序言:七十年代末曹阔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子隔披,更是在濱河造成了極大的恐慌赃份,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奢米,死亡現(xiàn)場(chǎng)離奇詭異抓韩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鬓长,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門谒拴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涉波,你說我怎么就攤上這事英上。” “怎么了啤覆?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵苍日,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我窗声,道長(zhǎng)相恃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任笨觅,我火速辦了婚禮拦耐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屋摇。我一直安慰自己揩魂,他們只是感情好幽邓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布炮温。 她就那樣靜靜地躺著,像睡著了一般牵舵。 火紅的嫁衣襯著肌膚如雪柒啤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天畸颅,我揣著相機(jī)與錄音担巩,去河邊找鬼。 笑死没炒,一個(gè)胖子當(dāng)著我的面吹牛涛癌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拳话,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼先匪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弃衍,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤呀非,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后镜盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岸裙,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年速缆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了降允。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艺糜,死狀恐怖拟糕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倦踢,我是刑警寧澤送滞,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站辱挥,受9級(jí)特大地震影響犁嗅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晤碘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一褂微、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧园爷,春花似錦宠蚂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扰楼,卻和暖如春呀癣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弦赖。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工项栏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蹬竖。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓沼沈,卻偏偏與公主長(zhǎng)得像流酬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子列另,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

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