ThreadPoolExecutor 執行緒池異常消失之刨根問底

一、情景復現

昨天,公司一個同事,急急忙忙的跑過來找我,說他的項目,出現了一個非常詭異的BUG,不知道什麼情況?

同事:我用五個執行緒計算學生各個科目的成績,最後匯總,本地都是正常的,但是一到測試環境就少了一科成績,也沒拋出異常,什麼鬼?
油七:任務執行緒怎麼做的?執行緒異常處理了嗎?為啥不列印日誌呢?靈魂三連擊,哈哈哈(開玩笑的,這不是我的處事風格)
油七:行,咱們先看一下程式碼…,一頓掃描占卜之後,大致知道啥情況了。
同事:哥,我這程式還有救嗎,客戶下了死命令,今天解決啊。
油七:沒事,小夥子,不要慌,你先把執行緒池這裡 submit 提交改成 execute 試一下
五分鐘之後…
同事:卧槽,拋出異常了,我這裡計算邏輯有問題,666,這是啥原因啊,為啥我 submit 提交,異常不拋出來啊?
油七:嗯,這個問題…

.

執行緒池

二、程式模擬

因為同事的程式碼邏輯比較繞,不便於咱們復現問題,因此我寫了一個簡單的問題實例,作為本篇文章分析的依據。程式計算用除法代替,除數取到了 0,按道理應該拋出ArithmeticException。

模擬程式碼

程式碼如下:

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ExceptionMissMain {

	public static class Task implements Runnable {
		String name;
		int a, b;

		public Task(String name, int a, int b) {
			this.name = name;
			this.a = a;
			this.b = b;
		}

		@Override
		public void run() {
			double c = a / b;
			System.out.println("科目:" + name + ", 成績:" + c);
		}

	}

	public static void main(String[] args) throws InterruptedException {
		ThreadPoolExecutor es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
		for (int i = 0; i < 5; i++) {
			es.submit(new Task(String.valueOf(i), 100, i));
			//es.execute(new Task(String.valueOf(i), 100, i));
			Thread.sleep(2000);
		}
	}
}

結果輸出

submit方式

科目:1, 成績:100.0
科目:2, 成績:50.0
科目:3, 成績:33.0
科目:4, 成績:25.0

缺少一科成績,程式運行無異常拋出

execute方式

Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at com.tiny.juc.boot.pool.ExceptionMissMain$Task.run(ExceptionMissMain.java:30)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:830)

科目:1, 成績:100.0
科目:2, 成績:50.0
科目:3, 成績:33.0
科目:4, 成績:25.0

缺少一科成績,程式運行異常拋出

三、刨根問底

看到上面兩種方式提交任務,輸出結果的不同,submit方式異常沒有了,execute方式拋出了異常,很多人肯定都出現了疑問?

執行緒池

別糾結了,直接動手刨墳吧,看一看源程式碼中兩個方式究竟是如何實現的,不就真相大白了嗎?just do it!我們採用斷點調試的方式,一步一步查看程式運行的過程。

源碼追擊

execute實現

1.首先咱們來看一下execute方法的實現,發現程式正常會進入addWorker方法

執行緒池

2.咱們來看一下addWorker方法,做了哪些事情?觀察下面的程式碼,我們會發現,addWorker方法先創建了一個Worker對象,並且將傳入的Runnable類型的task傳入到新建的Worker中,然後再從Worker對象中拿出thread變數,再調用了當前Worker的thread的start方法。疑問:start()方法運行的是什麼程式碼?,Worker對象創建都幹了什麼事情?Worker對象的thread是怎麼創建的?

執行緒池
執行緒池

3.帶著第二步的疑問,咱們再來一次,這次進到 new Worker 裡面,看一下。我們會發現,Worker對象新建的時候,將自己作為目標對象創建了一個執行緒,並且賦給了Worker中的thread,我們看到Worker類實現了Runnable介面,所以也就是說上一步裡面 t.start() 方法,調用的就是目標對象 Worker 自己的 run 方法。

執行緒池

4.為了驗證第三步的解釋,我們在 Thread 類中 run 方法與 Worker 類中的 run 方法,分別打上斷點,再運行。發現,確實和我們預想的一樣,程式先進入了 Thread 類中run 方法,後調用了Worker類中的 run 方法,繼而調用了Worker類中 runWorker 方法。

執行緒池
執行緒池

5.那麼現在,我們再看一下runWorker幹了什麼事情?我們發現runWorker獲取了Worker對象的Runnable task(也就是我們創建的任務),並且調用了我們任務的run 方法。

執行緒池
執行緒池

6.OK,我們現在只需要看一下,runWorker task.run()方法調用這裡的異常處理,就明白了。我們發現,此處運行有異常捕獲,try catch 了Throwable 異常,且向上拋出了,而我們的程式除數取到 0 的異常ArithmeticException,也包括在其中。

執行緒池
執行緒池

注釋:看到這我們就明白了,前面的程式為什麼execute方法會拋出異常了吧,行吧,都散了吧。什麼,我才剛看爽,你就叫我走?還有submit呢,為啥不拋異常啊,什麼情況還沒說呢,別想溜。。。好吧,咱們繼續看下 submit的底層實現。

submit實現

1.首先咱們來看一下submit方法的實現,發現程式會將我們提交的任務通過newTaskFor方法轉換成FutureTask
2.任務轉換成FutureTask後會調用與前面一樣的execute 方法

執行緒池
執行緒池

3.看到這我們就知道了,也就是說後面還是重複著前面execute執行相同的邏輯,只不過參數變成了FutureTask,那麼最後在runWorker方法裡面 task.run() 那裡,會走FutureTask類的 run 方法,去調用我們定義的任務。
4.所以我們去FutureTask類中,看一下 run方法的實現。我們發現run 方法中 try catch了異常,並且調用了setException 方法,但是在setException方法中,將異常賦給了outcome,未見其他處理。

執行緒池

5.最後我們看一下FutureTask整個類中outcome 出現的地方,發現在get 方法中通過調用 report 方法返回了 outcome。

執行緒池

6.所以我們在程式那裡,通過get方法去接收,看一下出現什麼結果?結果同execute方法一樣出現了異常。

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at com.tiny.juc.boot.pool.ExceptionMissMain.main(ExceptionMissMain.java:39)

注釋:看到這我們終於明白,submit與execute方法實現上的差異了,以及前文的程式程式碼為什麼submit提交不拋出異常,而execute提交拋出異常了吧。

四、總結

1)submit方法,針對異常資訊捕獲後調用setException 輸出到FutureTask 中的outcome;
2)任務如果是用submit方法提交的,那就用futureTask的get方法去接收;
3)execute方法會將任務的異常資訊,向上拋出;
4)使用執行緒池時,需要小心謹慎,做好程式的異常處理,日誌記錄;

.

執行緒池