這是why的第 69 篇原創文章html
一個編號爲 8073704 的 JDK BUG,將串聯起個人這篇文章。java
也就是下面的這個連接。c++
https://bugs.openjdk.java.net/browse/JDK-8073704
面試
這個 BUG 在 JDK 9 版本中進行了修復。也就是說,若是你用的 JDK 8,也許會遇到這樣的問題。segmentfault
先帶你們看看這個問題是怎麼樣的:api
這個 BUG 說:FutureTask.isDone 方法在任務尚未完成的時候就會返回 true。併發
能夠看到,這是一個 P4 級別(優先級不高)的 BUG,這個 BUG 也是分配給了 Doug Lea,由於 FutureTask 類就是他寫的:oracle
響應了國家政策:誰污染,誰治理。less
這個 BUG 的做者 Martin 老哥是這樣描述的:svg
下面我會給你們翻譯一下他要表達的東西。
可是在翻譯以前,我得先作好背景鋪墊,以避免有的朋友看了後一臉懵逼。
若是要懂他在說什麼,那我必須得再給你看個圖片,這是 FutureTask 的文檔描述:
看 Martin 老哥提交的 BUG 描述的時候,得對照着狀態圖和狀態對應的數字來看。
他說 FutureTask#isDone 方法如今是這樣寫的:
他以爲從源碼來看,是隻要當前狀態不等於 NEW(即不等於0)則返回 true,表示任務完成。
他以爲應該是這樣寫:
這樣寫的目的是除了判斷了 NEW 狀態以外,還判斷了兩個中間狀態:COMPLETING 和 INTERRUPTING。
那麼除去上面的三個狀態以外呢,就只剩下了這四個狀態:
這四個狀態能夠表明一個任務的最終狀態。
固然,他說上面的代碼還有優化空間,好比下面這樣,代碼少了,可是理解起來也得多轉個彎:
state>COMPLETING,知足這個條件的狀態只有下面這幾種:
而這幾種中,只有 INTERRUPTING 是一箇中間態,因此他用後面的 != 排除掉了。
這樣就是代碼簡潔了,可是理解起來多轉個小彎。可是這兩段代碼表示的含義是如出一轍的。
好了,關於這個 BUG 的描述就是這樣的。
彙總爲一句話就是,這個 Martin 老哥認爲:
FutureTask.isDone 方法在任務尚未完成的時候,好比仍是 COMPLETING 和 INTERRUPTING 的時候就會返回 true,這樣是不對的。這就是 BUG。
僅從 isDone 源碼中那段 status != NEW 的代碼,我認爲這個 Martin 老哥說的確實沒有問題。由於確實有兩個中間態,這段源碼中是沒有考慮的。
接下來,咱們就圍繞着這個問題進行展開,看看各位大神的討論。
首先,第一個發言的哥們是 Pardeep,是在這個問題被提出的 13 天以後:
我沒有太 get 到這個哥們回答的點是什麼啊。
他說:咱們應該去看一下 isDone 方法的描述。
描述上說:若是一個任務已完成,調用這個方法則返回true。而完成除了是正常完成外,還有多是任務異常或者任務取消致使的完成,這些都算完成。
我以爲他的這個回答和問題有點對不上號,感受是答非所問。
就當他拋出了一個關於 isDone 方法的知識點吧。
三天後,第二個發言的哥們叫作 Paul,他的觀點是這樣的:
首先,他說咱們不須要檢查 INTERRUPING 這個中間狀態。
由於若是一個任務處於這個狀態,那麼獲取結果的時候必定是拋出 CancellationException。
叫咱們看看 isCancelled 方法和 get 方法。
那咱們先看看 isCancelled 方法:
直接判斷了狀態是否大於等於 CANCELLED,也就是判斷了狀態是不是這三種中的一個:
判斷任務是否取消(isCancelled)的時候,並無對 INTERRUPING 這個中間狀態作特殊處理。
按照這個邏輯,那麼判斷任務是否完成(isDone)的時候,也不須要對 INTERRUPING 這個中間狀態作特殊處理。
接着,咱們看看 get 方法。
get 方法最終會調用這個 report 方法:
若是變量 s (即狀態)是 INTERRUPING (值是 5),那麼是大於 CANCELLED (值是 4)狀態的,則拋出 CancellationException (CE)異常。
因此,他以爲對於 INTERRUPING 狀態沒有必要進行檢測。
由於若是你調用 isCancelled 方法,那麼會告訴你任務取消了。
若是你調用 get 方法,會拋出 CE 異常。
因此,綜上所述,我認爲 Paul 這個哥們的邏輯是這樣的:
咱們做爲使用者,最終都會調用 get 方法來獲取結果,假設在調用 get 方法以前。咱們用 isCancelled 或者 isDone 判斷了一下任務的狀態。
若是當前狀態好死不死的就是 INTERRUPING 。那麼調用 isCancelled 返回 true,那按照正常邏輯,是不會繼續調用 get 方法的。
若是調用的是 isDone ,那麼也返回 true,就會去調用 get 方法。
因此在 get 方法這裏保證了,就算當前處於 INTERRUPING 中間態,程序拋出 CE 異常就能夠了。
所以,Paul 認爲若是沒有必要檢測 INTERRUPING 狀態的話,那麼咱們就能夠把代碼從:
簡化爲:
可是,這個哥們還說了一句話來兜底。
他說:Unless i have missed something subtle about the interactions
除非我沒有注意到一些很是小的細節問題。你看,說話的藝術。話都被他一我的說完了。
好了,Paul 同窗發言完畢了。42 分鐘以後,一個叫 Chris 的小老弟接過了話筒,他這樣說的:
我以爲吧,保羅說的挺有道理的,我同意他的建議。
可是吧,我也以爲咱們在討論的是一個很是細節,很是小的問題,我不知道,就算如今這樣寫,會致使任何問題嗎?
寫到這裏,先給你們捋一下:
因而他們以爲 isDone 方法應該修改爲這樣:
因此,如今只剩下一箇中間狀態是有爭議的了:COMPLETING 。
對於剩下的這個中間狀態,一位叫作 David 的靚仔,在三小時後發表了本身的意見:
他上來就是一個暴擊,直截了當的說:我認爲在座的各位都是垃圾。
好吧,他沒有這樣說。因此你看,仍是要多學學英語,否則我騙了你,你還不知道。
其實他說的是:我認爲沒有必要作任何改變。
COMPLETING 狀態是一個轉瞬即逝的過渡狀態,它表明咱們已經有最終狀態了,可是在設置最終狀態開始和結束的時間間隙內有一個瞬間狀態,它就是 COMPLETING 狀態。
其實你是能夠經過 get 方法知道任務是不是完成了,經過這個方法你能夠得到最終的正確答案。
由於 COMPLETING 這個轉瞬即逝的過渡狀態是不會被程序給檢測到的。
David 靚仔的回答在兩個半小時候獲得了大佬的確定:
Doug Lea 說:如今源碼裏面是故意這樣寫的,緣由就是 David 這位靚仔說的,我寫的時候就是這樣考慮過的。
另外,我以爲這個 BUG 的提交者本身應該解釋咱們爲何須要修改這部分代碼。
其實 Doug 的言外之意就是:你說這部分有問題,你給我舉個例子,別隻是整理論的,你弄點代碼給我看看。
半小時以後,這個 BUG 的提交者回復了:
intentional 知道是啥意思不?
害,我又得兼職教英語了:
他說:哦,原來是故意的呀。
這句話,你用不一樣的語氣能夠讀出不一樣的含義。
我這裏傾向於他以爲既然 Doug 當初寫這段代碼的時候考慮到了這點,他分析以後以爲本身這樣寫是沒有問題的,就這樣寫了。
好嘛,前面說 INTERRUPING 不須要特殊處理,如今說 COMPLETING 狀態是檢測不到的。
那就沒得玩了。
事情如今看起來已是被定性了,那就是不須要進行修改。
可是就在這時 Paul 同窗殺了個回馬槍,應該也是前面的討論激發了他的思路,你不是說檢測不出來嗎,你不是說 get 方法能夠得到最終的正確結果嗎?
那你看看我這段代碼是什麼狀況:
代碼是這樣的,你們能夠直接粘貼出來,在 JDK 8/9 環境下分別運行一下:
public static void main(String[] args) throws Exception { AtomicReference<FutureTask<Integer>> a = new AtomicReference<>(); Runnable task = () -> { while (true) { FutureTask<Integer> f = new FutureTask<>(() -> 1); a.set(f); f.run(); } }; Supplier<Runnable> observe = () -> () -> { while (a.get() == null); int c = 0; int ic = 0; while (true) { c++; FutureTask<Integer> f = a.get(); while (!f.isDone()) {} try { /* Set the interrupt flag of this thread. The future reports it is done but in some cases a call to "get" will result in an underlying call to "awaitDone" if the state is observed to be completing. "awaitDone" checks if the thread is interrupted and if so throws an InterruptedException. */ Thread.currentThread().interrupt(); f.get(); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (InterruptedException e) { ic ++; System.out.println("InterruptedException observed when isDone() == true " + c + " " + ic + " " + Thread.currentThread()); } } }; CompletableFuture.runAsync(task); Stream.generate(observe::get) .limit(Runtime.getRuntime().availableProcessors() - 1) .forEach(CompletableFuture::runAsync); Thread.sleep(1000); System.exit(0); }
先看一下這段代碼的核心邏輯:
首先標號爲 ① 的地方是兩個計數器,c 表明的是第一個 while 循環的次數,ic 表明的是拋出 InterruptedException(IE) 的次數。
標號爲 ② 的地方是判斷當前任務是不是完成狀態,若是是,則繼續往下。
標號爲 ③ 的地方是先中斷當前線程,而後調用 get 方法獲取任務結果。
標號爲 ④ 的地方是若是 get 方法拋出了 IE 異常,則在這裏進行記錄,打印日誌。
須要注意的是,若是打印日誌了,說明了一個問題:
前面明明 isDone 方法返回 true 了,說明方法執行完成了。可是我調用 get 方法的時候卻拋出了 IE 異常?
這你怕是有點說不通吧!
JDK 8 的運行結果我給你們截個圖。
這個異常是在哪裏被拋出來的呢?
awaitDone 方法的入口處,就先檢查了當前線程是否被中斷,若是被中斷了,那麼拋出 IE 異常:
而代碼怎麼樣才能執行到 awaitDone 方法呢?
任務狀態是小於等於 COMPLETING 的時候。
在示例代碼中,前面的 while 循環中的 isDone 方法已經返回了 true,說明當前狀態確定不是 NEW。
那麼只剩下個什麼東西了?
就只有一個 COMPLETING 狀態了。
小樣,這不就是監測到了嗎?
在這段示例代碼出來後的第 8 個小時,David 靚仔又來講話了:
他要表達的意思,我理解的是這樣的:
在 j.u.c 包裏面,優先檢查線程中斷狀態是很常見的操做,由於相對來講,會致使線程中斷的地方很是的少。
可是不能由於少,咱們就不檢查了。
咱們仍是得對其進行了一個優先檢查,告知程序當前線程是否發生了中斷,便是否有繼續往下執行的意義。
可是,在這個場景中,當前線程中斷了,但並不能表示 Future 裏面的 task 任務的完成狀況。這是兩個不相關的事情。
即便當前線程中斷了,可是 task 任務仍然能夠繼續完成。可是執行 get 方法的線程被中斷了,因此可能會拋出 InterruptedException。
所以,他給出的解決建議是:
能夠選擇優先返回結果,在 awaitDone 方法的循環中把檢查中斷的代碼挪到後面去。
五天以後,以前 BUG 的提交者 Martin 同窗又來了:
他說他改變主意了。
改變什麼主意了?他以前的主意是什麼?
在 Doug 說他是故意這樣寫的以後,Martin 說:
It's intentional。哦,原來是故意的呀。
那個時候他的主意就是:大佬都說了,這樣寫是考慮過的,確定沒有問題。
如今他的主意是:若是 isDone 方法返回了 true,那麼 get 方法應該明確的返回結果值,而不會拋出 IE 異常。
須要注意的是,這個時候對於 BUG 的描述已經發生變化了。
從「FutureTask.isDone 方法在任務尚未完成的時候就會返回 true」變成了「若是 isDone 方法返回了 true,那麼 get 方法應該明確的返回結果值,而不會拋出 IE 異常」。
而後 David 靚仔給出了一個最簡單的解決方案:
最簡單的解決方案就是先檢查狀態,再檢查當前線程是否中斷。
而後,這個 BUG 由 Martin 同窗進行了修復:
修復的代碼能夠先不看,下面一小節我會給你們作個對比。
他修復的同時還當心翼翼的要求 Doug 祝福他,爲他站個臺。
最後,Martin 同窗說他已經提交給了 jsr166,預計在 JDK 9 版本進行修復。
出於好奇,我在 JDK 的源碼中搜索了一下 Martin 同窗的名字,本覺得是個青銅,沒想到是個王者,失敬失敬:
既然說在 JDK 9 中對該 BUG 進行了修復,那麼帶你們對比一下 JDK 9/8 的代碼。
java.util.concurrent.FutureTask#awaitDone:
能夠看到,JDK 9 把檢查是否中斷的操做延後了一步。
代碼修改成這樣後,把以前的那段示例代碼放到 JDK 9 上面跑一下,你會驚奇的發現,沒有拋出異常了。
由於源碼裏面判斷 COMPLETING 的操做在判斷線程中斷標識以前:
我想就不須要我再過多解釋了吧。
而後多說一句 JDK 9 如今的 FutureTask#awaitDone 裏面有這樣的一行註釋:
它說:isDone 方法已經告訴使用者任務已經完成了,那麼調用 get 方法的時候咱們就不該該什麼都不返回或者拋出一個 IE 異常。
這行註釋想要表達的東西,就是上面一小節的 BUG 裏面咱們在討論的事情。寫這行註釋的人,就是 Martin 同窗。
當我瞭解了這個 BUG 的前因後果以後,又忽然間在 JDK 9 的源碼裏面看到這個註釋的時候,有一種很神奇的感受。
就是一種源碼說:you feel me?
我立刻心照不宣:I get you。
挺好。
在 JDK 9 的註釋裏面還有這個詞彙:
spurious wakeup,虛假喚醒。
若是你以前不知道這個東西的存在,那麼恭喜你,又 get 到了一個你基本上用不到的知識點。
除非你本身須要在代碼中用到 wait、notify 這樣的方法。
哦,也不對,面試的時候可能會用到。
「虛假喚醒」是怎麼一回事呢,我給你看個例子:
java.lang.Thread#join(long) 方法:
這裏爲何要用 while 循環,而不是直接用 if 呢?
由於循環體內有調用 wait 方法。
爲何調用了 wait 方法就必須用 while 循環呢?
別問,問就是防止虛假喚醒。
看一下 wait 方法的 javadoc:
一個線程能在沒有被通知、中斷或超時的狀況下喚醒,也即所謂的「虛假喚醒」,雖然這點在實踐中不多發生,可是程序應該循環檢測致使線程喚醒的條件,並在條件不知足的狀況下繼續等待,來防止虛假喚醒。
因此,建議寫法是這樣的:
在 join 方法中,isAlive 方法就是這裏的 condition does not hold。
在《Effective Java》一書中也有提到「虛假喚醒」的地方:
書中的建議是:沒有理由在新開發的代碼中使用 wait、notify 方法,即便有,也應該是極少了,請多使用併發工具類。
再送你一個面試題:爲何 wait 方法必須放在 while 循環體內執行?
如今你能回答的上來這個問題了吧。
關於「虛假喚醒」就說這麼多,有興趣的同窗能夠再去仔細瞭解一下。
好好的說着 JDK 的 FutureTask 呢,怎麼忽然轉彎到 Netty 上了?
由於 Netty 裏面,其核心的 Future 接口實現中,犯了一個基本的邏輯錯誤,在實現 cancel 和 isDone 方法時違反了 JDK 的約定。
這是一個讓 Netty 做者也感到驚訝的錯誤。
先看看 JDK Future 接口中,對於 cancel 方法的說明:
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html
文檔的方法說明上說:若是調用了 cancel 方法,那麼再調用 isDone 將永遠返回 true。
看一下這個測試代碼:
能夠看到,在調用了 cancel 方法後,再次調用 isDone 方法,返回的確實 false。
這個點我是好久以前在知乎的這篇文章上看到的,和本文討論的內容有一點點相關度,我就又翻了出來,多說了一嘴。
有興趣的能夠看看:《一個讓Netty做者也感到驚訝的錯誤》
好啦,才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在留言區提出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。