前言
因為這是之前面試的一個題目,所以印象比較深刻,前幾天寫了一篇文章:ThreadPoolExcutor 線程池 異常處理 (上篇) 中已經介紹了線程池異常的一些問題以及一步步分析了里面的一些源代碼,今天就來繼續說下如何防范這種情況。
結論
這里直接拋出結論,然后再一個個分析:
- 在我們提供的Runnable的run方法中捕獲任務代碼可能拋出的所有異常,包括未檢測異常
- 使用ExecutorService.submit執行任務,利用返回的Future對象的get方法接收拋出的異常,然后進行處理
- 重寫ThreadPoolExecutor.afterExecute方法,處理傳遞到afterExecute方法中的異常
- 為工作者線程設置UncaughtExceptionHandler,在uncaughtException方法中處理異常 (不推薦)
分析解讀
Runnable的run方法中捕獲任務代碼可能拋出的所有異常
這個其實最簡單,但是往往面試官問這個問題 考察的點也不在這里。具體的方式可以參考我之前的一篇文章:論如何優雅的自定義ThreadPoolExecutor線程池
核心代碼如下:
使用ExecutorService.submit執行任務,利用返回的Future對象的get方法接收拋出的異常
1, 使用submit執行異步任務,然后通過Future的get方法來接收異常。演示如下:
沖圖片可以看到,使用了get方法后,這里直接接收到了異常信息。
2, 這里newTaskFor返回的是FutureTask,然后傳遞給了execute方法:
3, 接着我們繼續往下跟蹤execute方法,發現這里調用的是ThreadExecutor中的execute方法,在ThreadPoolExcutor 線程池 異常處理 (上篇) 我們已經分析過這里,最終會到addWorker方法中執行線程的start()方法,因為我們在上一張圖片傳遞的是FutureTask, 所以我們繼續跟蹤FutureTask中的run方法:
4, 到了FutureTask.run() 方法中,一切似乎都已經明了,這里會有catch捕獲當前線程拋出的異常,緊接着我們看看setException做了什么事情:
5,setExcetion首先是將一個異常信息賦值給一個全局變量outcome,並且將全局的任務狀態state字段通過CAS更新為3(異常狀態)
然后最后做一些清理工作。
6,finishCompletion后續是做一些線程池的清理工作,這里涉及到線程池以及線程池中的等待隊列的操作,不清楚的同學可以看下線程池實現代碼。到了這里線程池中的線程執行已經完畢了,下面再去跟蹤一下FutureTask.get()方法。
7,這里是FutureTask.get()的底層實現,這里其實會拿上面的setException方法中設置的outcome和state做一些邏輯判斷,到了這里就直接往上拋出了異常,所以我們在最開始的main方法中才能夠捕獲到這個異常。
重寫ThreadPoolExecutor.afterExecute方法,處理傳遞到afterExecute方法中的異常
這里為何要重寫afterExecute方法呢?因為線程執行完畢后一定會執行此方法,源碼如下:
所以我們可以重寫此方法來達到接收異常的目的。
為工作者線程設置UncaughtExceptionHandler,在uncaughtException方法中處理異常 (不推薦)
1,我們在之前ThreadExecutor->Worker->run方法中直接往上拋出了異常,但是這些異常拋到哪里了呢?
2,通過查詢JVM的一些資料,最終的異常會到Thread.dispatchUncaughtException中,如下圖:
3,所以當我們自定義UncaughtExceptionHandler時就可以捕獲到
具體測試代碼如下:
/**
* 測試singleThreadPool異常問題
*
* @author: wangmeng
* @date: 2019/3/25 23:40
*/
public class ThreadPoolException {
private final static Logger LOGGER = LoggerFactory.getLogger(ThreadPoolException.class);
public static void main(String[] args) throws InterruptedException {
ExecutorService execute = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(new MyHandler()).build());
execute.execute(new Runnable() {
@Override
public void run() {
LOGGER.info("=====11=======");
}
});
TimeUnit.SECONDS.sleep(5);
execute.execute(new Run1());
}
private static class Run1 implements Runnable {
@Override
public void run() {
int count = 0;
while (true) {
count++;
LOGGER.info("-------222-------------{}", count);
if (count == 10) {
System.out.println(1 / 0);
try {
} catch (Exception e) {
LOGGER.error("Exception",e);
}
}
if (count == 20) {
LOGGER.info("count={}", count);
break;
}
}
}
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}
上面說了其實是不推薦重寫UncaughtExceptionHandler 的,因為UncaughtExceptionHandler 只有在execute.execute()
方法中才生效,在execute.submit
中是無法捕獲到異常的。
由於本人水平有限,文章中如果有不嚴謹的地方還請提出來,願聞其詳。