當前位置:編程學習大全網 - 源碼下載 - 源代碼泄漏再現

源代碼泄漏再現

前幾天,我在JDK線程池看到了壹個BUG。我去了解了壹下,找到了癥結所在。我認為這個BUG屬於線程池方法的壹個不合理設計,並且官方在知道這個BUG後表示:確實是BUG,但是我不會修復。就當是特色吧。

在我向妳詳細介紹這個BUG之前,讓我問妳壹個問題:

這東西,老八股文,比我入行的時間還長,只好張口就來:

這個BUG的觸發條件之壹隱藏在這個DiscardPolicy中。

但是妳看源代碼的時候,這個東西是壹個空方法。有什麽不好?

它的問題在於,它是壹個空方法,並且靜默地處理異常。

別急,讓我慢慢給妳擺好。

與BUG對應的鏈接是這樣的:

標題大概是說:哦,老朋友們,聽我說。我發現線程池的拒絕策略DiscardPolicy可能會導致線程在遇到invokerAll方法時壹直阻塞。

然後註意BUG描述部分的這兩段話:

這兩段揭示了兩個信息:

所以我們要去這個BUG第壹次出現的地方。這是鏈接:

從題目來看,這兩個問題很像,都有invokerAll和block,只是觸發條件不同。

壹種是丟棄策略拒絕策略,另壹種是關閉方法。

所以我的策略是先帶妳了解這個shutdownNow方法,讓妳更好的理解DiscardPolicy帶來的問題。

本質上,他們說的是同壹件事。

在這個與shutdownNow相關的BUG描述中,提問者給了他壹個測試用例,我稍微改了壹下就用上了。

代碼貼在這裏,妳也可以去妳當地:

然後向妳解釋測試代碼在做什麽。

首先,在標簽為①的地方,將10個可調用類型的任務塞進列表。

妳有這麽多任務做什麽?

肯定是扔進線程池了吧?

因此,在標記為②的地方,建立了壹個2線程2核心線程的線程池。線程池的invokerAll方法在線程中調用:

這個方法是做什麽的?

執行壹組給定的任務,並在所有任務完成後返回壹個包含其狀態和結果的未來列表。

也就是說,線程啟動時,線程池會逐個執行列表中的任務,執行後返回壹個未來列表。

我們寫代碼的時候,可以用這個列表知道這批任務是否完成了。

但是,朋友們,但是,啊,註意,在我的情況下,我根本不關心invokerAll方法的返回值。

我關心的是invokerAll方法執行後輸出的這句話:

好,現在這個程序有什麽問題?

妳看不出來,是嗎?

我也看不出來,因為根本沒毛病,程序也能正常運行。結束:

然後,我修改了程序,添加了以下代碼行,標記為③:

這裏所謂的是線程池的關閉方法。目的是讓程序在線程池處理完任務後退出。

拜托,這個程序怎麽了?

我肯定妳看不到,對吧?

我也沒有,因為根本沒毛病,程序也能正常運行。結束:

好了,接下來,我又要開始變身了。

程序變成這樣:

註意,我在這裏使用shutdownNow方法,這意味著我想立即關閉前面的線程池,然後讓整個程序退出。

那麽這個程序有什麽問題呢?

確實有問題,肉眼看確實很難看,但是我們可以先看看運行結果:

結果還是很好觀察的。

沒有“invokeAll returned”輸出,程序沒有退出。

那麽問題來了:妳覺得這是BUG嗎?

不管什麽原因,從現象上看,這是壹個BUG妥妥的,對吧?

我調用了shutdownNow,我只想立即關閉線程池,然後讓整個程序退出。結果任務沒有執行,程序卻沒有退出,這不是我們所期望的。

所以,大膽壹點,這是BUG!

讓我們看另壹個關於shutdownNow和shutdown方法輸出的比較圖,它更直觀:

至於這兩種方法的區別,我就不說了。不知道的話就上網查壹查,背壹背。

反正現在BUG可以穩定重現了。

下壹步是找出根本原因。

如何找到根本原因?

先想想這個問題:程序應該退出但沒有退出。這是否意味著仍然有線程在運行,或者說,有非守護線程在運行?

對了,這裏很容易想到。

看線程棧。

妳怎麽想呢?

相機,我的朋友。我們的老夥計,經常出現在以前的文章中,只是它:

這麽輕輕壹碰就能看出有壹根線不對:

它處於等待狀態,通過堆棧信息可以壹目了然的定位到導致它進入這種狀態的代碼,就是invokeAll方法的244行,也就是這行代碼:

既然問題出在invokeAll方法上,妳就要明白這個方法是幹什麽的。

標註①的地方是把進來的任務封裝成壹個Future對象,先放在壹個列表裏,然後調用execute方法,也就是扔進線程池執行。

這個操作特別像直接調用線程池的submit()方法。讓我給妳壹個比較:

標記②的地方是循環前的未來列表。如果未來的執行沒有完成,則調用未來的get方法來阻塞等待結果。

從堆棧信息來看,線程被阻塞在未來的get方法中,說明未來還沒有被執行。

為什麽沒有執行?

好了,讓我們回到測試代碼的地方:

10個任務,用2個核心線程扔進線程池。

是不是線程池中有兩個可以被線程執行,剩下的八個進入隊列?

好吧,我問妳:調用shutdownNow後,工作線程是不是直接枯竭了?是不是沒有資源去執行剩下的八條?

另壹方面,即使只有1個任務沒有執行?invokeAll方法中的future.get()是否也被阻塞?

但是,朋友們,但是,啊,在BUG這麽清楚的情況下,上面這個案子居然被政府推翻了。

發生了什麽事?

我給妳看看官方回復。

哦,不好意思,不是老板,是官方巨頭馬丁和道格的回復:

馬丁說:老鐵,我看了妳的代碼,感覺沒毛病?聽我說,shutdownNow方法會返回壹個尚未執行的任務列表。所以妳必須對現在關閉的回歸做些什麽。

道格說,馬丁是對的。順便說壹句:

他們參考這個列表。也就是說,他寫代碼的時候就考慮到了這種情況,所以他把沒有執行的任務全部返回給調用者。

shutdownNow方法有壹個返回值。我之前沒有註意到這個細節:

但是如果妳仔細看這個返回值,它是列表中的壹個Runnable。不是未來,所以不能調用future.cancel()方法。

那麽得到這個返回值後應該如何取消任務呢?

這是壹個好問題。因為提問者也有這樣壹個問題:

看到巨人說要對返回值進行操作後,他壹臉疑惑的回答:請註意,shutdownNow方法返回的是壹個列表。至少對我來說,我不知道如何取消這些任務。是否應該在文檔中描述?

馬丁哥覺得這個回復真的有點混亂,他做了如下回復:

線程池提交任務有兩種方式。

如果使用execute()方法提交壹個可運行任務,那麽shutdownNow將返回壹個尚未執行的可運行任務列表。

如果使用submit()方法提交可運行的任務,它將被封裝為FutureTask對象,因此調用shutdownNow方法將返回尚未執行的未來任務列表:

也就是說,shutdownNow方法返回的列表集可能包含Runnable,也可能包含FutureTask,這取決於將任務扔進線程池時調用的是什麽方法。

FutureTask是Runnable的子類:

因此,基於Martin兄弟的陳述和他提供的代碼,我們可以如下修改測試用例:

遍歷shutdownNow方法返回的列表集合,然後判斷是否是未來,如果是,就轉換成未來,然後調用它的cancel方法。

這樣程序才能正常運行並結束。

從這個角度來看,似乎不是BUG,可以通過編碼來避免。

但是,朋友們,但是啊,前面都是我的伏筆,然後劇情開始反轉。

讓我們回到這個鏈接:

這個鏈接中提到了壹個線程池拒絕策略DiscardPolicy。

只要我稍微修改壹下我們的演示程序,觸發線程的拒絕策略,之前的bug就真的是繞不過去的bug了。

應該怎麽改?

非常簡單,只需更改線程池:

用兩個核心線程替換我們以前的線程池,用壹個自定義線程池替換無限隊列長度。

這個自定義線程池的核心線程數、最大線程數和隊列長度都是1,采用的線程拒絕策略是DiscardPolicy。

其他地方不動代碼,整個代碼就變成這樣了。我把代碼貼出來給妳看,妳就可以直接運行了:

那麽讓我們先運行程序,看看結果:

嘿,發生什麽事了?

顯然我處理了shutdownNow的返回值。為什麽程序沒有輸出“invokeAll returned”並且在invokeAll方法上被阻塞?

就算我們不知道程序為什麽沒有停止,從性能的角度來看,這個東西壹定是bug吧

接下來我帶大家分析壹下為什麽會出現這種現象。

首先,我問妳,在我們的例子中,這個線程池最多能容納多少個任務?

我最多只能接2個任務嗎?

我最多只能接2個任務。這是否意味著我不能處理8個任務,需要實現線程池拒絕策略?

但是我們的拒絕策略是什麽呢?

DiscardPolicy,實現如下:靜默處理,丟棄任務,不拋出異常:

好了,妳接著想,shutdownNow返回什麽?是線程池中尚未執行的任務,即隊列中的任務嗎?

但是隊列裏最多只有壹個任務,回來給妳取消也沒用。

因此,這個案例與不處理shutdownNow的返回值無關。

關鍵是被拒絕的八個任務,或者說關鍵是觸發DiscardPolicy拒絕策略。

觸發壹次的效果和觸發多次是壹樣的。在我們自定義線程池和invokeAll方法的這個場景中,只要任何任務都是靜默處理的,這甚至是在玩雞蛋。

為什麽這麽說?

讓我們看壹下默認線程池拒絕策略AbortPolicy的實現:

被拒絕執行後會拋出異常,然後在invokeAll方法中被捕獲,所以不會阻塞:

如果是靜默處理,妳沒有地方讓這個靜默處理的Future拋出異常,也沒有地方調用它的cancel方法,所以這裏會壹直被阻塞。

這是壹個BUG。

那麽官方是如何回復這個BUG的呢?

馬丁回答:我認為應該在文件中說明,拒絕策略在真實場景中很少使用,不建議所有人使用。不然妳把它當成特色嗎?

我覺得言下之意是:我知道這是個BUG,但是妳得用DiscardPolicy,壹個實際編碼中不會用到的拒絕策略。我認為妳故意裝了竊聽器。

我對這個答復不滿意。

馬丁兄弟不知道壹些事情。我們面試的時候,有個八股文環節,有個老八股文題是這樣的:

如果妳足夠聰明,當妳自定義線程池拒絕策略時,妳寫了壹個花哨的拒絕策略,但它相當於DiscardPolicy。

也就是說,妳沒有把它放入隊列,拋出壹個異常。不管妳的代碼有多花哨,妳還是會有這個問題。

所以我覺得還是invokeAll方法的設計問題。不應設計調用線程之外的其他線程無法訪問的未來。

這違背了未來物體的設計理念。

所以我說這是BUG,是設計問題。

什麽,妳問我怎麽設計?

抱歉,無可奉告。

  • 上一篇:Aauto快壹點的直播間自動扣詞用什麽軟件?
  • 下一篇:如何在mac上使用git連接私有的gitlab服務器
  • copyright 2024編程學習大全網