Java 線程池的異常處理機制

一、前言

線程池技術(shù)是服務(wù)器端開發(fā)中常用的技術(shù)偏灿。不論是直接還是間接钝的,各種服務(wù)器端功能的執(zhí)行總是離不開線程池的調(diào)度铆遭。關(guān)于線程池的各種文章,多數(shù)是關(guān)注任務(wù)的創(chuàng)建和執(zhí)行方面碗脊,對于異常處理和任務(wù)取消(包括線程池關(guān)閉)關(guān)注的偏少橄妆。

接下來,本文將從 Java 原生線程矢劲、兩種主要線程池 ThreadPoolExecutorScheduledThreadPoolExecutor 這三方面介紹 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耘柱。ScheduledFutureTaskFutureTask 的子類棍现,所以镜遣,異常也會被復(fù)制給 outcome

但是谎僻,這里還是有一些差異的寓辱。在使用 ThreadPoolExecutor.submitScheduledThreadPoolExecutor.schedule 方法時秫筏,我們可以通過這兩個方法返回的 Future 來獲得執(zhí)行結(jié)果挎挖,這包括正常結(jié)果航夺,也包括異常結(jié)果。但是始衅,對于 ScheduledThreadPoolExecutor.scheduleWithFixedDelayscheduleAtFixedRate 這兩個方法缭保,其返回的 Future 只會用來取消任務(wù),而不是得到結(jié)果诸老。原因也很容易理解彻亲,因為這兩個方法執(zhí)行的是定時任務(wù),是反復(fù)執(zhí)行的畸肆。這也是為什么這兩個方法的任務(wù)定義使用了 Runnable 接口宙址,而不是有返回值的 Callable 接口抡砂。因此,對于這兩個方法來說注益,在 Runnable.run 方法中加 try...catch 是必須的丑搔,否則很有可能出錯了卻毫不知情。

五煮仇、結(jié)論

Thread 中谎仲,我們可以通過 UncaughtExceptionHandler 來實現(xiàn)默認的異常處理機制。但是在使用 ThreadPoolExecutorScheduledThreadPoolExecutor 這兩個 JDK 最主要的線程池時夹姥,使用 UncaughtExceptionHandler 是不合適的。所以佃声,try...catch 往往是不可避免的圾亏,否則你的任務(wù)很有可能失敗的悄無聲息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夭问,一起剝皮案震驚了整個濱河市缰趋,隨后出現(xiàn)的幾起案子陕见,更是在濱河造成了極大的恐慌,老刑警劉巖灰粮,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粘舟,死亡現(xiàn)場離奇詭異佩研,居然都是意外死亡,警方通過查閱死者的電腦和手機晰骑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門些侍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來政模,“玉大人蚂会,你說我怎么就攤上這事〕煤铮” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵娱挨,是天一觀的道長捕犬。 經(jīng)常有香客問我碉碉,道長,這世上最難降的妖魔是什么贴届? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任蜡吧,我火速辦了婚禮,結(jié)果婚禮上元潘,老公的妹妹穿的比我還像新娘耀鸦。我一直安慰自己袖订,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布上沐。 她就那樣靜靜地躺著楞艾,像睡著了一般硫眯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上两入,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音紧武,去河邊找鬼敏储。 笑死,一個胖子當(dāng)著我的面吹牛妥箕,可吹牛的內(nèi)容都是我干的酝碳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼返奉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起芽偏,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎污尉,沒想到半個月后膀哲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡被碗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年某宪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锐朴。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡兴喂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出焚志,到底是詐尸還是另有隱情衣迷,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布壶谒,位于F島的核電站,受9級特大地震影響膳沽,放射性物質(zhì)發(fā)生泄漏佃迄。R本人自食惡果不足惜泼差,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呵俏。 院中可真熱鬧,春花似錦滔灶、人聲如沸普碎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽麻车。三九已至,卻和暖如春斗这,著一層夾襖步出監(jiān)牢的瞬間动猬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工表箭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赁咙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓免钻,卻偏偏與公主長得像彼水,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子极舔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

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

  • 執(zhí)行多線程并發(fā)任務(wù)的時候凤覆,如果任務(wù)類型相同,一般會考慮使用線程池拆魏,一方面利用了并發(fā)的優(yōu)勢盯桦,一方面避免創(chuàng)建大量線程得...
    德彪閱讀 23,434評論 2 19
  • 前段時間遇到這樣一個問題,有人問微信朋友圈的上傳圖片的功能怎么做才能讓用戶的等待時間較短渤刃,比如說一下上傳9張圖片,...
    加油碼農(nóng)閱讀 1,186評論 0 2
  • 并發(fā)的學(xué)習(xí)與使用系列 第五篇 線程池的技術(shù)背景 在面向?qū)ο缶幊讨杏德停瑒?chuàng)建和銷毀對象是很費時間的,因為創(chuàng)建一個對象要獲...
    SilenceDut閱讀 1,063評論 1 24
  • 上集回顧:上集我們稍做講解了Future與Executor Executors 創(chuàng)建并得到ThreadPoolEx...
    蘇先生Tongson閱讀 706評論 0 2
  • 前言 JDK中為我們提供了一個并發(fā)線程框架溪掀,它是的我們可以在有異步任務(wù)或大量并發(fā)任務(wù)需要執(zhí)行時可以使用它提供的線程...
    Justlearn閱讀 1,782評論 0 10