JVM原始碼分析之Object.wait/notify(All)完全解讀

PerfMa發表於2020-06-19

概述

本文其實一直都想寫,因為各種原因一直拖著沒寫,直到開公眾號的第一天,有朋友再次問到這個問題,這次讓我靜心下來準備寫下這篇文章,本文有些東西是我自己的理解,比如為什麼JDK一開始要這麼設計,初衷是什麼,沒怎麼去找相關資料,所以只能談談自己的理解,所以大家看到文章之後可以談談自己的看法,對於實現部分我倒覺得說清楚問題不大,code is here,看明白了就知道怎麼回事了。

Object.wait/notify(All)大家都知道主要是協同執行緒處理的,大家用得也很多,大概邏輯和下面的用法差不多

image.png

看到上面程式碼,你會有什麼疑惑嗎?至少我會有幾個問題會問自己: * 為什麼進入wait和notify的時候要加synchronized鎖 * 既然加了synchronized鎖,那當某個執行緒呼叫了wait的時候明明還在synchronized塊裡,其他執行緒怎麼進入到鎖裡去執行notify的 * 為什麼wait方法可能會丟擲InterruptedException異常 * 如果有多個執行緒都進入wait狀態,那某個執行緒呼叫notify喚醒執行緒時是否按照順序喚起那些wait執行緒 * wait的執行緒是在某個執行緒執行完notify之後立馬就被喚起嗎 * notifyAll又是怎麼實現全喚起的 * wait的執行緒是否會影響load

如果上面這些問題也都是你想了解的,那這篇文章或許能給你一個答案。

為何要加synchronized鎖

從實現上來說,這個鎖至關重要,正因為這把鎖,才能讓整個wait/notify玩轉起來,當然我覺得其實通過其他的方式也可以實現類似的機制,不過hotspot至少是完全依賴這把鎖來實現wait/notify的。

如果要我們來實現這種機制我們會怎麼去做,我們知道wait/notify是為了執行緒間協作而設計的,當我們執行wait的時候讓執行緒掛起,當執行notify的時候喚醒其中一個掛起的執行緒,那需要有個地方來儲存物件和執行緒之間的對映關係(可以想象一個map,key是物件,value是一個執行緒列表),當呼叫這個物件的wait方法時,將當前執行緒放到這個執行緒列表裡,當呼叫這個物件的notify方法時從這個執行緒列表裡取出一個來讓其繼續執行,這樣看來是可行的,也比較簡單,那現在的問題這種對映關係放到哪裡。而synchronized正好也是為執行緒間協作而設計的,上面碰到的問題它也要解決,或許正因為這樣wait和notify的實現就直接依賴synchronzied(monitorenter/monitorexit是jvm規範裡要求要去實現的)來實現了,這只是我的理解,可能初衷不是這個原因,這其實也是這篇文章遲遲未寫的一個原因吧,因為我無法取證自己的理解是對的,歡迎各位在這塊談談自己的見解。

wait方法執行後未退出同步塊,其他執行緒如何進入同步塊

這個問題其實要回答很簡單,因為在wait處理過程中會臨時釋放同步鎖,不過需要注意的是當某個執行緒呼叫notify喚起了這個執行緒的時候,在wait方法退出之前會重新獲取這把鎖,只有獲取了這把鎖才會繼續執行,想象一下,我們知道wait的方法是被monitorenter和monitorexit包圍起來,當我們在執行wait方法過程中如果釋放了鎖,出來的時候又不拿鎖,那在執行到monitorexit指令的時候會發生什麼?當然這可以做相容,不過這實現起來還是很奇怪的。

為什麼wait方法可能丟擲InterruptedException異常

這個異常大家應該都知道,當我們呼叫了某個執行緒的interrupt方法時,對應的執行緒會丟擲這個異常,wait方法也不希望破壞這種規則,因此就算當前執行緒因為wait一直在阻塞,當某個執行緒希望它起來繼續執行的時候,它還是得從阻塞態恢復過來,因此wait方法被喚醒起來的時候會去檢測這個狀態,當有執行緒interrupt了它的時候,它就會丟擲這個異常從阻塞狀態恢復過來。

這裡有兩點要注意: * 如果被interrupt的執行緒只是建立了,並沒有start,那等他start之後進入wait態之後也是不能會恢復的 * 如果被interrupt的執行緒已經start了,在進入wait之前,如果有執行緒呼叫了其interrupt方法,那這個wait等於什麼都沒做,會直接跳出來,不會阻塞

被notify(All)的執行緒有規律嗎

這裡要分情況: * 如果是通過notify來喚起的執行緒,那先進入wait的執行緒會先被喚起來 * 如果是通過nootifyAll喚起的執行緒,預設情況是最後進入的會先被喚起來,即LIFO的策略

notify執行之後立馬喚醒執行緒嗎

其實這個大家可以驗證一下,在notify之後寫一些邏輯,看這些邏輯是在其他執行緒被喚起之前還是之後執行,這個是個細節問題,可能大家並沒有關注到這個,其實hotspot裡真正的實現是退出同步塊的時候才會去真正喚醒對應的執行緒,不過這個也是個預設策略,也可以改的,在notify之後立馬喚醒相關執行緒。

notifyAll是怎麼實現全喚起的

或許大家立馬想到這個簡單,一個for迴圈就搞定了,不過在jvm裡沒實現這麼簡單,而是藉助了monitorexit,上面我提到了當某個執行緒從wait狀態恢復出來的時候,要先獲取鎖,然後再退出同步塊,所以notifyAll的實現是呼叫notify的執行緒在退出其同步塊的時候喚醒起最後一個進入wait狀態的執行緒,然後這個執行緒退出同步塊的時候繼續喚醒其倒數第二個進入wait狀態的執行緒,依次類推,同樣這這是一個策略的問題,jvm裡提供了挨個直接喚醒執行緒的引數,不過都很罕見就不提了。

wait的執行緒是否會影響load

這個或許是大家比較關心的話題,因為關乎系統效能問題,wait/nofity是通過jvm裡的park/unpark機制來實現的,在linux下這種機制又是通過pthread_cond_wait/pthread_cond_signal來玩的,因此當執行緒進入到wait狀態的時候其實是會放棄cpu的,也就是說這類執行緒是不會佔用cpu資源。

 

一起來學習吧:

PerfMa KO 系列課之 JVM 引數【Memory篇】

Hotspot GC研發工程師也許漏掉了一塊邏輯

相關文章