當前位置:首頁 > 公眾號精選 > 架構師社區(qū)
[導讀]作者之前寫過一篇文章《有的線程它死了,于是它變成一道面試題》,這是早期作品,遣詞造句,排版行文都有一點稚嫩,但承蒙厚愛,還是有很多人看過,甚至已經進入了某網紅公司的面試題庫里面。本文相當于是對上面這篇文章的一個補充。


一道面試題

我一年前寫過這篇文章《有的線程它死了,于是它變成一道面試題》,這是早期作品,遣詞造句,排版行文都有一點稚嫩,但是承蒙厚愛,還是有很多人看過。

甚至已經進入了某網紅公司的面試題庫里面。

關于多線程中拋異常的這個面試題,我再說最后一次!

本文相當于是對上面這篇文章的一個補充。

現(xiàn)在先回顧一下這篇文章拋出的問題和問題的答案:

一個線程池中的線程異常了,那么線程池會怎么處理這個線程?

這個題是我遇到的一個真實的面試題,當時并沒有回答的很好。然后通過上面的文章,我在源碼中尋找到了答案。

先給大家看兩個案例。

sayHi 方法是會拋出運行時異常的。

當執(zhí)行方式是 execute 方法時,在控制臺會打印堆棧異常:

關于多線程中拋異常的這個面試題,我再說最后一次!

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

關于多線程中拋異常的這個面試題,我再說最后一次!

那么怎么獲取這個 submit 方法提交時的異常信息呢?

得調用返回值 future 的 get 方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

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

關于多線程中拋異常的這個面試題,我再說最后一次!

然后一個讀者找我聊天,說為什么他這樣寫,通過 future.get 方法沒有拋出異常呢,和我文章里面說的不一樣呢?

我說:那肯定是你操作不對,你把代碼發(fā)給我看看。

關于多線程中拋異常的這個面試題,我再說最后一次!

然后我收到了一份這樣的代碼:

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()?+?",執(zhí)行方式:"?+?name?+?"】";
????????System.out.println(printStr);
????????throw?new?RuntimeException(printStr?+?",我異常啦!哈哈哈!");
????}
}

這個程序的輸出結果是這樣的:

關于多線程中拋異常的這個面試題,我再說最后一次!

我尋思這沒毛病呀,這不是很正常嗎?不就是應該這樣輸出嗎?

那個哥們說:和你說的不一樣啊,你說的是調用 future.get 方法的時候會拋出異常的?我這里并沒有輸出“future.get Exception”,說明 future.get 方法沒有拋出異常。

我回答到:你這不是把會拋出運行時異常的 sayHi 方法用 try/catch 代碼塊包裹起來了嗎?異常在子線程里面就處理完了,也就不會封裝到 Future 里面去了。你把 try/catch 代碼塊去掉,異常就會封裝到 Future 里面了。

關于多線程中拋異常的這個面試題,我再說最后一次!

過了一小會,他應該是實驗完了,又找過來了。

他說:牛逼呀,確實是這樣的。那你的這個面試題是有問題的啊,描述不清楚,正確的描述應該是一個線程池中的線程拋出了未經捕獲的運行時異常,那么線程池會怎么處理這個線程?

看到他的這個回復的時候,我竟然鼓起掌來,這屆讀者真是太嚴格了!但是他說的確實是沒有錯,嚴謹點好。

關于多線程中拋異常的這個面試題,我再說最后一次!

他還追問到:怎么實現(xiàn)的呢?為什么當 submit 方法提交任務的時候,子線程捕獲了異常,future.get 方法就不拋出異常了呢?

其實聽到這個問題的時候都把我干懵了。

這問法,難道你是想再拋一次異常出來?

其實大家按照正常的思維去想,都能知道如果子線程捕獲了一次,future.get 方法就不應該拋出異常了。

所以,現(xiàn)在的問題是,這個小小的功能,在線程池里面是怎么實現(xiàn)的?

現(xiàn)在的面試題在原來的基礎上再加一層:

好,你說當執(zhí)行方法是 submit 的時候,如果子線程拋出未經捕獲的運行時異常,將會被封裝到 Future 里面?那么如果子線程捕獲了異常,該異常還會封裝到 Future 里面嗎?是怎么實現(xiàn)的呢

尋找答案-FUTURE

來,一起去源碼里面尋找答案。

現(xiàn)在是用 submit 的方式往線程池里面提交任務,而執(zhí)行的這個任務會拋出運行時異常。

對于拋出的這個異常,我們分為兩種情況:

  • 子線程中捕獲了異常,則調用返回的 future 的 get 方法,不會拋出異常。

  • 子線程中沒有捕獲異常,則調用返回的 future 的 get 方法,會拋出異常。

關于多線程中拋異常的這個面試題,我再說最后一次!

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

這個 Future 是一個接口:

關于多線程中拋異常的這個面試題,我再說最后一次!

而這個接口有非常多的實現(xiàn)類。我們找哪個實現(xiàn)類呢?

就是下面這個實現(xiàn)類:

java.util.concurrent.FutureTask

至于是怎么找到它的,你慢慢往后看就知道了。

先看看 FutureTask 的 get 方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

get 方法的邏輯很簡單,首先判斷當前狀態(tài)是否已完成,如果不是,則進入等待,如果是,則進入 report 方法。

一進 get 方法,我們就看到了 state 這個東西,這是 FutureTask 里面一個非常重要的東西:

關于多線程中拋異常的這個面試題,我再說最后一次!

在 FutureTask ?里面,一共有 7 種狀態(tài)。這 7 種狀態(tài)之間的流轉關系已經在注釋里面寫清楚了。

狀態(tài)之間只會按照這四個流程去流轉。

關于多線程中拋異常的這個面試題,我再說最后一次!

所以,一目了然,一個任務的終態(tài)有四種:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED。

而我們主要關心 NORMAL、EXCEPTIONAL。

所以再回頭看看 get 方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

如果當前狀態(tài)是小于 COMPLEING 的。

也就是當前狀態(tài)只能是 NEW 或者 COMPLEING,總之就是任務還沒有完成。所以進入 awaitDone 方法。這個方法不是本文關心的地方,接著往下看。

程序能往下走,說明當前的狀態(tài)肯定是下面圈起來的狀態(tài)中的某一個:

關于多線程中拋異常的這個面試題,我再說最后一次!

記住這幾種狀態(tài),然后看這個 report 方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

這個方法是干啥的?

注解說的很清楚了:對于已經完成了的 task,返回其結果或者拋出異常。

這里面的邏輯就很簡單了,把 outcome 變量賦值給 x 。

然后判斷當前狀態(tài),如果是 NORMAL,即 2,說明正常完成,直接返回 x。

如果是大于等于 CANCELLED,即大于等于 4 ,即這幾種狀態(tài),就拋出 CancellationException。

剩下的情況就拋出 ExecutionException。

關于多線程中拋異常的這個面試題,我再說最后一次!

而這個“剩下的情況”是什么情況?

不就只剩下一個 EXCEPTIONAL 的情況了。

所以,經過前面的描述,我們可以總結一下。

當 FutureTask 的 status 為 NORMAL 時正常返回結果,當 status 為 EXCEPTIONAL 時拋出異常。

而當終態(tài)為 NORMAL 或者 EXCEPTIONAL 時,按照注釋描述,狀態(tài)的流程只能是這樣的:

關于多線程中拋異常的這個面試題,我再說最后一次!

那么到底是不是這樣的呢?

這就需要我們去線程池里面驗證一下了。

尋找答案-線程池

先回答上一節(jié)的一個問題:我怎么知道是看 Future 這個接口的 FutureTask 這個實現(xiàn)類的:

關于多線程中拋異常的這個面試題,我再說最后一次!

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

關于多線程中拋異常的這個面試題,我再說最后一次!

可以看到,F(xiàn)utureTask 的構造方法里面默認了狀態(tài)為 NEW。

然后直接在 runWorker 方法的 task.run 方法處打上斷點:

關于多線程中拋異常的這個面試題,我再說最后一次!

這個 task 是一個 FutureTask,所以 run 方法其實是 FutureTask 的 run 方法。

跟著斷點進去之后,就是 FutureTask 的 run 方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

答案都藏在這個方法里面。

java.util.concurrent.FutureTask#run

標號為 ① 的地方是執(zhí)行我們的任務,call 的就是示例代碼里面的 sayHi 方法。

如果提交的任務( sayHi 方法)拋出的運行時異常沒有被捕獲,則會在標號為 ② 的這個 catch 里面被捕獲。然后執(zhí)行標號為 ② 的這個代碼。

如果提交的任務?sayHi 方法捕獲了運行時異常,則會進入標號為 ③ 的這個邏輯里面。

我們分別看一下標號為 ② 和 ③ 的邏輯:

關于多線程中拋異常的這個面試題,我再說最后一次!

首先,兩個方法都是先進行一個 cas 的操作,把當前 FutureTask 的 status 字段從 NEW 修改為 COMPLETING 。

完成了狀態(tài)流轉的這一步:

關于多線程中拋異常的這個面試題,我再說最后一次!

注意這里,如果 cas 操作失敗了,則不會進行任何操作。

cas 操作失敗了,說明什么呢?

說明當前的狀態(tài)是 CANCELLED 或者 INTERRUPTING 或者 INTERRUPTED。

也就是這個任務被取消了或者被中斷了。

那還設置結果干啥,沒有任何卵用,對不對。

關于多線程中拋異常的這個面試題,我再說最后一次!

如果 cas 操作成功,接著往下看,可以看到雖然入參不一樣了,但是都賦給了 outcome 變量,這個變量,在上一節(jié)的 report 方法出現(xiàn)過,還記得嗎?能不能呼應上?

接下來就是狀態(tài)接著往下流轉。

set 方法表示正常結束,狀態(tài)流轉到 NORMAL。

setException 方法表示任務出現(xiàn)異常,狀態(tài)流轉到 EXCEPTIONAL。

所以經過 FutureTask 的 run 方法后,如果任務沒有被中斷或者取消,則會通過 setException 或者 set 方法完成狀態(tài)的流轉和 outcome 參數(shù)的設置:

關于多線程中拋異常的這個面試題,我再說最后一次!

而到底是調用 setException 方法還是 set 方法,取決于標號為 ① 的地方是否會拋出異常。

即取決于任務體是否會拋出異常。

假設 sayHi 方法是這樣的,會拋出運行時異常:

關于多線程中拋異常的這個面試題,我再說最后一次!

而通過 submit 方法提交任務時寫法分別如下:

如果是標號為 ① 的寫法,則會進入 setException 方法。

如果是標號為 ② 的寫法,則會進入 set 方法。

所以,你現(xiàn)在再回去看看這個題目:

當執(zhí)行方法是 submit 的時候,如果子線程拋出未經捕獲的運行時異常,將會被封裝到 Future 里面,那么如果子線程捕獲了異常,該異常還會封裝到 Future 里面嗎?是怎么實現(xiàn)的呢?

現(xiàn)在是不是很清晰了。

如果子線程捕獲了異常,該異常不會被封裝到 Future 里面。是通過 FutureTask 的 run 方法里面的 setException 和 set 方法實現(xiàn)的。在這兩個方法里面完成了 FutureTask 里面的 outcome 變量的設置,同時完成了從 NEW 到 NORMAL 或者 EXCEPTIONAL 狀態(tài)的流轉。

線程池拒絕異常

寫文章的時候我突然又想到一個問題。

不論是用 submit 還是 execute 方法往線程池里面提交任務,如果由于線程池滿了,導致拋出拒絕異常呢?

RejectedExecutionException 異常也是一個 RuntimeException:

關于多線程中拋異常的這個面試題,我再說最后一次!

那么對于這個異常,如果我們不進行捕獲,是不是也不會打印呢?

假設你不知道這個問題,你就分析一下,從會和不會中猜一個唄。

我猜是會打印的。

因為假設讓我來提供一個這樣的功能,由于線程池飽和了而拒絕了新任務的提交,我肯定得給使用方一個提示。告訴他有的任務由于線程池滿了而沒有提交進去。

不然,使用者自己排查到這個問題后,肯定會說一聲:這什么傻逼玩意,把異常給吞了?

關于多線程中拋異常的這個面試題,我再說最后一次!

來,搞個 Demo 驗證一下:

關于多線程中拋異常的這個面試題,我再說最后一次!

我們定義的這個線程池最大容量是 7 個任務。

在循環(huán)體中扔 10 個比較耗時的任務進去。有 3 個任務它處理不了,那么肯定是會觸發(fā)拒絕策略的。

你覺得這個程序運行后會在控制臺打印異常日志嗎?會打印幾次呢?

看一下運行結果:

關于多線程中拋異常的這個面試題,我再說最后一次!

拋出了一次異常,執(zhí)行完成了 7 個任務。

我們并沒有捕獲異常,打印堆棧信息的相關代碼,那么這個異常是誰打印的?

如果你沒有捕獲異常,JVM 會幫你調用這個方法:

關于多線程中拋異常的這個面試題,我再說最后一次!

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

關于多線程中拋異常的這個面試題,我再說最后一次!

所以,當我們沒有捕獲異常的時候,會在這里打印一次堆棧日志。

而當我們捕獲了異常之后,改成這樣:

關于多線程中拋異常的這個面試題,我再說最后一次!

再次運行:

關于多線程中拋異常的這個面試題,我再說最后一次!

10 個任務,三次異常,完成了 7 個任務。

也不會讓 JVM 觸發(fā) dispatchUncaughtException 方法了。

而這個異常日志的打印和哪種方式提交任務沒有關系,不論哪種,只要你沒有捕獲異常,則都會觸發(fā) dispatchUncaughtException?方法。

終極答案

上面說這個例子,其實我就是想引出終極答案。

終極答案就是:dispatchUncaughtException 方法。

為什么這樣說呢?

我們現(xiàn)在把情況分為三種。

第一種:submit 方法提交一個會拋出運行時異常的任務,捕不捕獲異常都可以。

第二種:execute 方法提交一個會拋出運行時異常的任務,不捕獲異常。

第三種:submit 或者 execute 提交,讓線程池飽和之后拋出拒絕異常,代碼沒有捕獲異常。

第一種情況,無論如何都不會觸發(fā) dispatchUncaughtException 方法。因為 submit 方法提交,不論你捕獲與否,源碼里面都幫你捕獲了:

關于多線程中拋異常的這個面試題,我再說最后一次!

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

關于多線程中拋異常的這個面試題,我再說最后一次!

而我們自己沒有捕獲,所以會觸發(fā) dispatchUncaughtException 方法。

第三種情況,和第二種其實是一樣的。沒有捕獲,就會觸發(fā)。

那么我現(xiàn)在給你一段這樣的代碼:

關于多線程中拋異常的這個面試題,我再說最后一次!

你肯定知道這是會拋出異常的吧。

就像這樣式兒的:

關于多線程中拋異常的這個面試題,我再說最后一次!

我們完全沒有打印日志的代碼吧?

那你現(xiàn)在知道控制臺這個異常信息是怎么來的了不?

關于多線程中拋異常的這個面試題,我再說最后一次!

是不是平時根本就沒有注意這個點。

特別推薦一個分享架構+算法的優(yōu)質內容,還沒關注的小伙伴,可以長按關注一下:

關于多線程中拋異常的這個面試題,我再說最后一次!

關于多線程中拋異常的這個面試題,我再說最后一次!

關于多線程中拋異常的這個面試題,我再說最后一次!

長按訂閱更多精彩▼

關于多線程中拋異常的這個面試題,我再說最后一次!

如有收獲,點個在看,誠摯感謝

免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據媒體報道,騰訊和網易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據產業(yè)博覽會開幕式在貴陽舉行,華為董事、質量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據產業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經營業(yè)績穩(wěn)中有升 落實提質增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉