一道面試題
我一年前寫過這篇文章《有的拋異線程它死了,于是試題它變成一道面試題》,這是再說最后早期作品,遣詞造句,關于個面排版行文都有一點稚嫩,多線但是程中常的次承蒙厚愛,還是有很多人看過。
甚至已經進入了某網紅公司的面試題庫里面。

本文相當于是對上面這篇文章的一個補充。
現在先回顧一下這篇文章拋出的問題和問題的答案:
一個線程池中的線程異常了,那么線程池會怎么處理這個線程?
這個題是我遇到的一個真實的面試題,當時并沒有回答的很好。然后通過上面的文章,我在源碼中尋找到了答案。
先給大家看兩個案例。
sayHi 方法是會拋出運行時異常的。
當執行方式是 execute 方法時,在控制臺會打印堆棧異常:

當執行方式是 submit 方法時,在控制臺不會打印堆棧異常:

那么怎么獲取這個 submit 方法提交時的異常信息呢?
得調用返回值 future 的 get 方法:

具體原因,我在之前的文章里面詳細分析過,就不贅述了,直接看結論:

然后一個讀者找我聊天,說為什么他這樣寫,通過 future.get 方法沒有拋出異常呢,和我文章里面說的不一樣呢?
我說:那肯定是你操作不對,你把代碼發給我看看。

然后我收到了一份這樣的代碼:
public?class?ExecutorsTest?{
????public?static?void?main(String[]?args)?{
????????ThreadPoolExecutor?executorService?=?new?ThreadPoolExecutor(2,?2,
????????????????30,?TimeUnit.SECONDS,?new?ArrayBlockingQueue<>(10));
????????Future?future?=?executorService.submit(()?->?{
????????????try?{
????????????????sayHi("submit");
????????????}?catch?(Exception?e)?{
????????????????System.out.println("sayHi?Exception");
????????????????e.printStackTrace();
????????????}
????????});
????????try?{
????????????future.get();
????????}?catch?(Exception?e)?{
????????????System.out.println("future.get?Exception");
????????????e.printStackTrace();
????????}
????}
????private?static?void?sayHi(String?name)?throws?RuntimeException?{
????????String?printStr?=?"【thread-name:"?+?Thread.currentThread().getName()?+?",執行方式:"?+?name?+?"】";
????????System.out.println(printStr);
????????throw?new?RuntimeException(printStr?+?",我異常啦!哈哈哈!");
????}
}
這個程序的輸出結果是這樣的:

我尋思這沒毛病呀,這不是很正常嗎?不就是應該這樣輸出嗎?
那個哥們說:和你說的不一樣啊,你說的是調用 future.get 方法的時候會拋出異常的?我這里并沒有輸出“future.get Exception”,說明 future.get 方法沒有拋出異常。
我回答到:你這不是把會拋出運行時異常的 sayHi 方法用 try/catch 代碼塊包裹起來了嗎?異常在子線程里面就處理完了,也就不會封裝到 Future 里面去了。你把 try/catch 代碼塊去掉,異常就會封裝到 Future 里面了。

過了一小會,他應該是實驗完了,又找過來了。
他說:牛逼呀,確實是這樣的。那你的這個面試題是有問題的啊,描述不清楚,正確的描述應該是一個線程池中的線程拋出了未經捕獲的運行時異常,那么線程池會怎么處理這個線程?
看到他的這個回復的時候,我竟然鼓起掌來,這屆讀者真是太嚴格了!但是他說的確實是沒有錯,嚴謹點好。

他還追問到:怎么實現的呢?為什么當 submit 方法提交任務的時候,子線程捕獲了異常,future.get 方法就不拋出異常了呢?
其實聽到這個問題的時候都把我干懵了。
這問法,難道你是想再拋一次異常出來?
其實大家按照正常的思維去想,都能知道如果子線程捕獲了一次,future.get 方法就不應該拋出異常了。
所以,現在的問題是,這個小小的功能,在線程池里面是怎么實現的?
現在的面試題在原來的基礎上再加一層:
好,你說當執行方法是 submit 的時候,如果子線程拋出未經捕獲的運行時異常,將會被封裝到 Future 里面?那么如果子線程捕獲了異常,該異常還會封裝到 Future 里面嗎?是怎么實現的呢
尋找答案-FUTURE
來,一起去源碼里面尋找答案。
現在是用 submit 的方式往線程池里面提交任務,而執行的這個任務會拋出運行時異常。
對于拋出的這個異常,我們分為兩種情況:
子線程中捕獲了異常,則調用返回的 future 的 get 方法,不會拋出異常。
子線程中沒有捕獲異常,則調用返回的 future 的 get 方法,會拋出異常。

兩種情況都和 future.get 方法有關,那我們就從這個方法的源碼入手。
這個 Future 是一個接口:

而這個接口有非常多的實現類。我們找哪個實現類呢?
就是下面這個實現類:
java.util.concurrent.FutureTask
至于是怎么找到它的,你慢慢往后看就知道了。
先看看 FutureTask 的 get 方法:

get 方法的邏輯很簡單,首先判斷當前狀態是否已完成,如果不是,則進入等待,如果是,則進入 report 方法。
一進 get 方法,我們就看到了 state 這個東西,這是 FutureTask 里面一個非常重要的東西:

在 FutureTask ?里面,一共有 7 種狀態。這 7 種狀態之間的流轉關系已經在注釋里面寫清楚了。
狀態之間只會按照這四個流程去流轉。

所以,一目了然,一個任務的終態有四種:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED。
而我們主要關心 NORMAL、EXCEPTIONAL。
所以再回頭看看 get 方法:

如果當前狀態是小于 COMPLEING 的。
也就是當前狀態只能是 NEW 或者 COMPLEING,總之就是任務還沒有完成。所以進入 awaitDone 方法。這個方法不是本文關心的地方,接著往下看。
程序能往下走,說明當前的狀態肯定是下面圈起來的狀態中的某一個:

記住這幾種狀態,然后看這個 report 方法:

這個方法是干啥的?
注解說的很清楚了:對于已經完成了的 task,返回其結果或者拋出異常。
這里面的邏輯就很簡單了,把 outcome 變量賦值給 x 。
然后判斷當前狀態,如果是 NORMAL,即 2,說明正常完成,直接返回 x。
如果是大于等于 CANCELLED,即大于等于 4 ,即這幾種狀態,就拋出 CancellationException。
剩下的情況就拋出 ExecutionException。

而這個“剩下的情況”是什么情況?
不就只剩下一個 EXCEPTIONAL 的情況了。
所以,經過前面的描述,我們可以總結一下。
當 FutureTask 的 status 為 NORMAL 時正常返回結果,當 status 為 EXCEPTIONAL 時拋出異常。
而當終態為 NORMAL 或者 EXCEPTIONAL 時,按照注釋描述,狀態的流程只能是這樣的:

那么到底是不是這樣的呢?
這就需要我們去線程池里面驗證一下了。
尋找答案-線程池
先回答上一節的一個問題:我怎么知道是看 Future 這個接口的 FutureTask 這個實現類的:

submit 方法提交的時候把任務包裹了一層,就是用 FutureTask 包裹的:

可以看到,FutureTask 的構造方法里面默認了狀態為 NEW。
然后直接在 runWorker 方法的 task.run 方法處打上斷點:

這個 task 是一個 FutureTask,所以 run 方法其實是 FutureTask 的 run 方法。
跟著斷點進去之后,就是 FutureTask 的 run 方法:

答案都藏在這個方法里面。
java.util.concurrent.FutureTask#run
標號為 ① 的地方是執行我們的任務,call 的就是示例代碼里面的 sayHi 方法。
如果提交的任務( sayHi 方法)拋出的運行時異常沒有被捕獲,則會在標號為 ② 的這個 catch 里面被捕獲。然后執行標號為 ② 的這個代碼。
如果提交的任務(?sayHi 方法)捕獲了運行時異常,則會進入標號為 ③ 的這個邏輯里面。
我們分別看一下標號為 ② 和 ③ 的邏輯:

首先,兩個方法都是先進行一個 cas 的操作,把當前 FutureTask 的 status 字段從 NEW 修改為 COMPLETING 。
完成了狀態流轉的這一步:

注意這里,如果 cas 操作失敗了,則不會進行任何操作。
cas 操作失敗了,說明什么呢?
說明當前的狀態是 CANCELLED 或者 INTERRUPTING 或者 INTERRUPTED。
也就是這個任務被取消了或者被中斷了。
那還設置結果干啥,沒有任何卵用,對不對。

如果 cas 操作成功,接著往下看,可以看到雖然入參不一樣了,但是都賦給了 outcome 變量,這個變量,在上一節的 report 方法出現過,還記得嗎?能不能呼應上?
接下來就是狀態接著往下流轉。
set 方法表示正常結束,狀態流轉到 NORMAL。
setException 方法表示任務出現異常,狀態流轉到 EXCEPTIONAL。
所以經過 FutureTask 的 run 方法后,如果任務沒有被中斷或者取消,則會通過 setException 或者 set 方法完成狀態的流轉和 outcome 參數的設置:

而到底是調用 setException 方法還是 set 方法,取決于標號為 ① 的地方是否會拋出異常。
即取決于任務體是否會拋出異常。
假設 sayHi 方法是這樣的,會拋出運行時異常:

而通過 submit 方法提交任務時寫法分別如下:
如果是標號為 ① 的寫法,則會進入 setException 方法。
如果是標號為 ② 的寫法,則會進入 set 方法。
所以,你現在再回去看看這個題目:
當執行方法是 submit 的時候,如果子線程拋出未經捕獲的運行時異常,將會被封裝到 Future 里面,那么如果子線程捕獲了異常,該異常還會封裝到 Future 里面嗎?是怎么實現的呢?
現在是不是很清晰了。
如果子線程捕獲了異常,該異常不會被封裝到 Future 里面。是通過 FutureTask 的 run 方法里面的 setException 和 set 方法實現的。在這兩個方法里面完成了 FutureTask 里面的 outcome 變量的設置,同時完成了從 NEW 到 NORMAL 或者 EXCEPTIONAL 狀態的流轉。
線程池拒絕異常
寫文章的時候我突然又想到一個問題。
不論是用 submit 還是 execute 方法往線程池里面提交任務,如果由于線程池滿了,導致拋出拒絕異常呢?
RejectedExecutionException 異常也是一個 RuntimeException:

那么對于這個異常,如果我們不進行捕獲,是不是也不會打印呢?
假設你不知道這個問題,你就分析一下,從會和不會中猜一個唄。
我猜是會打印的。
因為假設讓我來提供一個這樣的功能,由于線程池飽和了而拒絕了新任務的提交,我肯定得給使用方一個提示。告訴他有的任務由于線程池滿了而沒有提交進去。
不然,使用者自己排查到這個問題后,肯定會說一聲:這什么傻逼玩意,把異常給吞了?
來,搞個 Demo 驗證一下:

我們定義的這個線程池最大容量是 7 個任務。
在循環體中扔 10 個比較耗時的任務進去。有 3 個任務它處理不了,那么肯定是會觸發拒絕策略的。
你覺得這個程序運行后會在控制臺打印異常日志嗎?會打印幾次呢?
看一下運行結果:

拋出了一次異常,執行完成了 7 個任務。
我們并沒有捕獲異常,打印堆棧信息的相關代碼,那么這個異常是誰打印的?
如果你沒有捕獲異常,JVM 會幫你調用這個方法:

而這個方法里面,會輸出錯誤堆棧:

所以,當我們沒有捕獲異常的時候,會在這里打印一次堆棧日志。
而當我們捕獲了異常之后,改成這樣:

再次運行:

10 個任務,三次異常,完成了 7 個任務。
也不會讓 JVM 觸發 dispatchUncaughtException 方法了。
而這個異常日志的打印和哪種方式提交任務沒有關系,不論哪種,只要你沒有捕獲異常,則都會觸發 dispatchUncaughtException?方法。
終極答案
上面說這個例子,其實我就是想引出終極答案。
終極答案就是:dispatchUncaughtException 方法。
為什么這樣說呢?
我們現在把情況分為三種。
第一種:submit 方法提交一個會拋出運行時異常的任務,捕不捕獲異常都可以。
第二種:execute 方法提交一個會拋出運行時異常的任務,不捕獲異常。
第三種:submit 或者 execute 提交,讓線程池飽和之后拋出拒絕異常,代碼沒有捕獲異常。
第一種情況,無論如何都不會觸發 dispatchUncaughtException 方法。因為 submit 方法提交,不論你捕獲與否,源碼里面都幫你捕獲了:

第二種情況,如果不捕獲異常,會觸發 dispatchUncaughtException 方法,因為 runWorker 方法的源碼里面雖然捕獲了異常,但是又拋出去了:

而我們自己沒有捕獲,所以會觸發 dispatchUncaughtException 方法。
第三種情況,和第二種其實是一樣的。沒有捕獲,就會觸發。
那么我現在給你一段這樣的代碼:

你肯定知道這是會拋出異常的吧。
就像這樣式兒的:

我們完全沒有打印日志的代碼吧?
那你現在知道控制臺這個異常信息是怎么來的了不?

是不是平時根本就沒有注意這個點。
特別推薦一個分享架構+算法的優質內容,還沒關注的小伙伴,可以長按關注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責聲明:本文內容由21ic獲得授權后發布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯系我們,謝謝!