一、前言
線程池技術是服務器端開發中常用的技術。不論是直接還是間接,各種服務器端功能的執行總是離不開線程池的調度。關於線程池的各種文章,多數是關注任務的創建和執行方面,對於異常處理和任務取消(包括線程池關閉)關注的偏少。接下來,本文將從 Java 原生線程、兩種主要線程池 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
這三方面介紹 Java 中線程的異常處理機制。
二、Thread
在談線程池的異常處理之前,我們先來看 Java 中線程中的異常是如何被處理的。大家都知道如何創建一個線程任務,代碼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
中執行 System.out.println
方法。后面不再解釋。如果這個任務拋出了異常,那又會怎樣,代碼2
-
Thread t = new Thread(() -> System.out.println(1 / 0));
-
t.start();
如果我們執行上面這段代碼,會在控制台上看到異常輸出。可能多數同學會對此不會覺得問題,但是問題在於,通常情況下絕大多數線上應用不會將控制台作為日志輸出地址,而是另有日志輸出。這種情況下,上面的代碼所拋出異常便會丟失。那為了將異常輸出到日志中,我們會這樣寫代碼:代碼3
-
Thread t = new Thread(() -> {
-
try {
-
System.out.println(1 / 0);
-
} catch (Exception e) {
-
LOGGER.error(e.getMessage(), e);
-
}
-
});
-
t.start();
-
這樣我們就能異常棧輸出到日志中,而不是控制台,從而避免異常的丟失。過了一段時間,問題又來了,可能好多線程任務默認的異常處理機制都是相同的。比如都是將異常輸出到日志文件。按照上面的寫法會造成重復代碼。雖然重復的不多,但是有代碼潔癖的小伙伴可能也會覺得不舒服。那我們該如何解決這個問題呢?其實 JDK 已經為我們想到了,Thread
類中有個接口 UncaughtExceptionHandler
。通過實現這個接口,並調用 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)
方法,我們就能為一個線程設置默認的異常處理機制,避免重復的 try...catch
了。除此以外,我們還可以通過 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)
設置全局的默認異常處理機制。此外,ThreadGroup
也實現了 UncaughtExceptionHandler
接口,所以通過 ThreadGroup
還可以為一組線程設置默認的異常處理機制。其實,之所以代碼2在執行之后我們能在控制台上看到異常,也是因為 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)) {
-
// 最終執行如下代碼
-
System.err.print("Exception in thread \"" + t.getName() + "\" ");
-
e.printStackTrace(System.err);
-
}
-
}
-
}
-
三、ThreadPoolExecutor
在 Java 5 發布之后,線程池便開始越來越廣泛地用於創建並發任務。多數時候,當說到 Java 的線程池時,我們一般指的就是 ThreadPoolExecutor
。那在 ThreadPoolExecutor
中是如何處理異常的呢?代碼5
-
Executors.newSingleThreadExecutor().execute(() -> {
-
throw new RuntimeException("My runtime exception");
-
});
上面的代碼的異常處理機制其實同直接使用 Thread
是一樣的。所以也有同樣的問題,異常信息無法反映在日志文件中。解決這個問題的方法同上一節一樣:在每個 Runnable
中編寫 try ... catch
語句;或者使用 UncaughtExceptionHandler
機制。我們先來看如何為線程池中的工作線程設置 UncaughtExceptionHandler
。
簡單來說,就是通過 ThreadFactory
。通過 ThreadPoolExecutor
的構造函數和 Executors
中的工具方法,我們都可以為新創建的線程池設置 ThreadFactory
。ThreadFactory
是個接口,它只定義了一個方法 Thread newThread(Runnable r)
。在這個方法中,我們可以為新創建出來的線程設置 UncaughtExceptionHandler
。當然,這樣寫起來顯得很麻煩,好在 Apache Commons 和 Google Guava 這兩個最有名的 Java 工具類庫都為我們提供了相應的類庫以簡化配置 ThreadFactory
的工作。下面以 Apache Commons 提供的 BasicThreadFactoryBuilder
為例,代碼6
-
ThreadFactory executorThreadFactory = new BasicThreadFactory.Builder()
-
.namingPattern( "task-scanner-executor-%d")
-
.uncaughtExceptionHandler( new LogUncaughtExceptionHandler(LOGGER))
-
.build();
-
Executors.newSingleThreadExecutor(executorThreadFactory);
此話怎講呢?其實 ThreadPoolExecutor
為執行並發任務提供了兩種方法:execute(Runnable)
和 submit(Callable/Runnable)
。之前的代碼示例只演示了執行 execute(Runnable)
時的情況。那在設置了默認的 UncaughtExceptionHandler
之后,當執行 submit(Callable/Runnable)
方法,拋出拋異常之后有會如何?看下面的代碼代碼7
-
ThreadFactory threadFactory = new ThreadFactoryBuilder()
-
.setUncaughtExceptionHandler( new LogExceptionHandler())
-
.build();
-
Executors.newSingleThreadExecutor(threadFactory)
-
.submit(() -> {
-
throw new RuntimeException("test");
-
});
上面的程序執行完之后,不會在控制台或日志中看到任何輸出,雖然設置了 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
方法是調用 execute
實現任務執行的。但是在調用 execute
之前,任務會被封裝進 FutureTask
類中,然后最終工作線程執行的是 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();
-
}
-
}
由上面的代碼可以看出,不同於直接調用 execute
方法,調用 submit
方法后,如果任務拋出異常,會被 setException
方法賦給代表執行結果的 outcome
變量,而不會繼續拋出。因此,UncaughtExceptionHandler
也沒有機會處理。如果想知道 submit
的執行結果是成功還是失敗,必須調用 Future.get()
方法。
從上面的分析中可以看出,使用 UncaughtExceptionHandler
,可以處理到使用 execute
方法執行任務所拋出的異常,但是對 submit
方法無效。那如果只是用 execute
方法,我們是否可以通過設置 UncaughtExceptionHandler
從而添加一種默認的異常處理機制,以避免重復的 try...catch
代碼呢?答案是不能。原因在於,如果在執行 execute
方法時不在 Runnable.run
方法中寫 try...catch
方法,自然異常會交由 UncaughtExceptionHandler
處理,但是,在這之前,線程的工作線程會因為異常而退出。雖然線程池會創建一個新的工作線程,但是如果這個步驟反復執行,效率自然會下降很多。
四、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
是另一種常用的線程池,常用了執行延遲任務或定時任務。常用的方法為 scheduleXXX
系列。那在這個線程池中異常是如何處理的呢?其實,如果看過前面的部分,到這里也基本能猜出來了。ScheduledThreadPoolExecutor
用來封裝任務的是 ScheduledFutureTask
。ScheduledFutureTask
是 FutureTask
的子類,所以,異常也會被復制給 outcome
。但是,這里還是有一些差異的。在使用 ThreadPoolExecutor.submit
和 ScheduledThreadPoolExecutor.schedule
方法時,我們可以通過這兩個方法返回的 Future
來獲得執行結果,這包括正常結果,也包括異常結果。但是,對於 ScheduledThreadPoolExecutor.scheduleWithFixedDelay
和 scheduleAtFixedRate
這兩個方法,其返回的 Future
只會用來取消任務,而不是得到結果。原因也很容易理解,因為這兩個方法執行的是定時任務,是反復執行的。這也是為什么這兩個方法的任務定義使用了 Runnable
接口,而不是有返回值的 Callable
接口。因此,對於這兩個方法來說,在 Runnable.run
方法中加 try...catch
是必須的,否則很有可能出錯了卻毫不知情。
五、結論
在Thread
中,我們可以通過 UncaughtExceptionHandler
來實現默認的異常處理機制。但是在使用 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
這兩個 JDK 最主要的線程池時,使用 UncaughtExceptionHandler
是不合適的。所以,try...catch
往往是不可避免的,否則你的任務很有可能失敗的悄無聲息。
原文鏈接:https://my.oschina.net/lifany/blog/884002
https://blog.csdn.net/u011635492/article/details/80328815