Java執行緒狀態以及 sheep()、wait()、yield() 的區別
前言
最近看到很多人都在討論多執行緒的問題,於是寫出了這篇部落格,希望可以幫到正在學習和使用這塊的朋友們,首先我們先看看兩個圖(兩個圖都來自其他碼農的分享)。
這兩個圖是一樣的邏輯,這裡一起羅列出來,下面讓我們用語句來簡單描述下兩個圖:
sleep 讓執行緒從 【running】 → 【阻塞態】 時間結束/interrupt → 【runnable】
wait 讓執行緒從 【running】 → 【等待佇列】notify → 【鎖池】 → 【runnable】
當我們看到這個圖的時候首先會看到執行緒的幾種狀態,下面讓我們先來分別說明一下:
執行緒共包括一下5種狀態
1. 新建、初始狀態(New) :執行緒物件被建立後就進入了新建狀態,Thread thread = new Thread();
2. 就緒(Runnable):也被稱之為“可執行狀態”,當執行緒被new出來後,其他的執行緒呼叫了該物件的start()方法,即thread.start(),此時執行緒位於“可執行執行緒池”中,只等待獲取CPU的使用權,隨時可以被CPU呼叫。進入就緒狀態的程序除CPU之外,其他執行所需的資源都已經全部獲得。
3. 執行(Running):執行緒獲取CPU許可權開始執行。注意:執行緒只能從就緒狀態進入到執行狀態。
4. 阻塞(Bloacked):阻塞狀態是執行緒因為某種原因放棄CPU的使用權,暫時停止執行,知道執行緒進入就緒狀態後才能有機會轉到執行狀態。
阻塞的情況分三種:
- 等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入“等待池中”。進入這個狀態後是不能自動喚醒的,必須依靠其他執行緒呼叫notify()或者notifyAll()方法才能被喚醒。
- 同步阻塞:執行的執行緒在獲取物件的(synchronized)同步鎖時,若該同步鎖被其他執行緒佔用,則JVM會吧該執行緒放入“鎖池”中。
- 其他阻塞:通過呼叫執行緒的sleep()或者join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新回到就緒狀態
以上三種阻塞狀態請參考上面的2個圖示來理解。
5. 死亡(Dead):執行緒執行完了或因異常退出了run()方法,則該執行緒結束生命週期。
wait(), notify(), notifyAll()等方法介紹
這三個方法都是定義到Object類中,wait的作用是噹噹前執行緒釋放它所持有的鎖進入等待狀態,而notify和notifyAll則是喚醒當前物件上的等待執行緒。
notify() —— 喚醒在此物件監視器上等待的單個執行緒。
notifyAll() —— 喚醒在此物件監視器上等待的所有執行緒。
wait() —— 讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法”,當前執行緒被喚醒(進入“就緒狀態”)。
wait(long timeout) —— 讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前執行緒被喚醒(進入“就緒狀態”)。
wait(long timeout, int nanos) —— 讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量”,當前執行緒被喚醒(進入“就緒狀態”)。
wait()會使“當前執行緒”等待,並且會釋放到它所佔用的“鎖標誌”,從而使執行緒所在物件中的其他synchronized資料可以被其他執行緒使用。
waite()和notify()因為會對物件的“鎖標誌”進行操作,所以它們必須在synchronized函式或synchronizedblock中進行呼叫。如果在non-synchronized函式或non-synchronizedblock中進行呼叫,雖然能編譯通過,但在執行時會發生IllegalMonitorStateException的異常。
負責喚醒等待執行緒的那個執行緒(我們稱為“喚醒執行緒”),它只有在獲取“該物件的同步鎖”(這裡的同步鎖必須和等待執行緒的同步鎖是同一個),並且呼叫notify()或notifyAll()方法之後,才能喚醒等待執行緒。雖然,等待執行緒被喚醒;但是,它不能立刻執行,因為喚醒執行緒還持有“該物件的同步鎖”。必須等到喚醒執行緒釋放了“物件的同步鎖”之後,等待執行緒才能獲取到“物件的同步鎖”進而繼續執行。
suspend()和 resume()方法
兩個方法配套使用,suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume()被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend()和 resume() 被用在等待另一個執行緒產生的結果的情形:測試發現結果還沒有產生後,讓執行緒阻塞,另一個執行緒產生了結果後,呼叫 resume()使其恢復。
注意區別:
初看起來wait() 和 notify() 方法與suspend()和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的suspend()及其它所有方法線上程阻塞時都不會釋放佔用的鎖(如果佔用了的話),而wait() 和 notify() 這一對方法則相反。
sleep() 和 yield()方法
這兩個方法都定義在Thread.java中
sleep()的作用是讓當前執行緒休眠(正在執行的執行緒主動讓出cpu,然後cpu就可以去執行其他任務),即當前執行緒會從“執行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,執行緒休眠的時候會大於或者等於該休眠時間,當時間過後該執行緒重新被會形式,他會由“阻塞狀態”程式設計“就緒狀態”,從而等待cpu的排程執行,注意:sleep方法只是讓出了cpu的執行權,並不會釋放同步資源鎖。
yield()的作用是讓步,它能夠讓當前執行緒從“執行狀態”進入到“就緒狀態”,從而讓其他等待執行緒獲取執行權,但是不能保證在當前執行緒呼叫yield()之後,其他執行緒就一定能獲得執行權,也有可能是當前執行緒又回到“執行狀態”繼續執行,注意:這裡我將上面的“具有相同優先順序”的執行緒直接改為了執行緒,很多資料都寫的是讓具有相同優先順序的執行緒開始競爭,但其實不是這樣的,優先順序低的執行緒在拿到cpu執行權後也是可以執行,只不過優先順序高的執行緒拿到cpu執行權的概率比較大而已,並不是一定能拿到。
舉個例子:一幫朋友在排隊上公交車,輪到Yield的時候,他突然說:我不想先上去了,咱們大家來競賽上公交車。然後所有人就一塊衝向公交車,
有可能是其他人先上車了,也有可能是Yield先上車了。
但是執行緒是有優先順序的,優先順序越高的人,就一定能第一個上車嗎?這是不一定的,優先順序高的人僅僅只是第一個上車的概率大了一點而已,
最終第一個上車的,也有可能是優先順序最低的人。並且所謂的優先順序執行,是在大量執行次數中才能體現出來的。
wait和sleep的區別
相同點:
1. 他們都是在多執行緒的環境下,都可以在程式的調用出阻塞指定的毫秒數並且返回
2. 兩個方法都可以通過interrupt()方法打斷執行緒的暫停狀態,但是執行緒會丟擲InterruptedException。需要注意的是,InterruptedException是執行緒自己從內部丟擲的,並不是interrupt()方法丟擲的。對某一執行緒呼叫 interrupt()時,如果該執行緒正在執行普通的程式碼,那麼該執行緒根本就不會丟擲InterruptedException。但是,一旦該執行緒進入到 wait()/sleep()/join()後,就會立刻丟擲InterruptedException 。
不同點:
1. Thread類的方法:sleep(),yield()
Object的方法:wait()和notify()、notifyAll()
2. 每個物件都有一個鎖來控制同步訪問。Synchronized關鍵字可以和物件的鎖互動,來實現執行緒的同步。 sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他執行緒可以使用同步控制塊或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用 。注意:wiat()必須放在synchronized block中,否則會在program runtime時扔出“java.lang.IllegalMonitorStateException”異常。
4. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
綜上可得兩者最大的區別:sleep()睡眠時,保持物件鎖,仍然佔有該鎖;而wait()睡眠時,釋放物件鎖。
注意:
第一:呼叫notify() 方法導致解除阻塞的執行緒是從因呼叫該物件的 wait()方法而阻塞的執行緒中隨機選取的,我們無法預料哪一個執行緒將會被選擇,所以程式設計時要特別小心,避免因這種不確定性而產生問題。
第二:除了notify(),還有一個方法 notifyAll()也可起到類似作用,唯一的區別在於,呼叫 notifyAll()方法將把因呼叫該物件的 wait()方法而阻塞的所有執行緒一次性全部解除阻塞。當然,只有獲得鎖的那一個執行緒才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend()方法和不指定超時期限的wait()方法的呼叫都可能產生死鎖。遺憾的是,Java並不在語言級別上支援死鎖的避免,我們在程式設計中必須小心地避免死鎖。
我有一個微信公眾號,經常會分享一些Java技術相關的乾貨;如果你喜歡我的分享,可以用微信搜尋“Java團長”或者“javatuanzhang”關注。