點擊上方“Coder編程”,選擇“置頂公眾號”
技術文章第一時間送達!

每天進步一點,不做curd工程師與Api調用工程師 歡迎訪問
個人博客網站:https://www.coder-programming.cn/?
?作者:liuxiaopeng? | http://www.cnblogs.com/paddix
一、線程的狀態
Java中線程中狀態可分為五種:New(新建狀態),Runnable(就緒狀態),Running(運行狀態),Blocked(阻塞狀態),Dead(死亡狀態)。
New:新建狀態,當線程創建完成時為新建狀態,即new Thread(...),還沒有調用start方法時,線程處于新建狀態。
Runnable:就緒狀態,當調用線程的的start方法后,線程進入就緒狀態,等待CPU資源。處于就緒狀態的線程由Java運行時系統的線程調度程序(thread scheduler)來調度。
Running:運行狀態,就緒狀態的線程獲取到CPU執行權以后進入運行狀態,開始執行run方法。
Blocked:阻塞狀態,線程沒有執行完,由于某種原因(如,I/O操作等)讓出CPU執行權,自身進入阻塞狀態。
Dead:死亡狀態,線程執行完成或者執行過程中出現異常,線程就會進入死亡狀態。
這五種狀態之間的轉換關系如下圖所示:

有了對這五種狀態的基本了解,現在我們來看看Java中是如何實現這幾種狀態的轉換的。
二、wait/notify/notifyAll方法的使用
1、wait方法:

JDK中一共提供了這三個版本的方法,
(1)wait()方法的作用是將當前運行的線程掛起(即讓其進入阻塞狀態),直到notify或notifyAll方法來喚醒線程.
(2)wait(long timeout),該方法與wait()方法類似,唯一的區別就是在指定時間內,如果沒有notify或notifAll方法的喚醒,也會自動喚醒。
(3)至于wait(long timeout,long nanos),本意在于更精確的控制調度時間,不過從目前版本來看,該方法貌似沒有完整的實現該功能,其源碼(JDK1.8)如下:
public?final?void?wait(long?timeout,?int?nanos)?throws?InterruptedException?{
????????if?(timeout?0)?{
????????????throw?new?IllegalArgumentException("timeout?value?is?negative");
????????}
????????if?(nanos?0?||?nanos?>?999999)?{
????????????throw?new?IllegalArgumentException(
????????????????????????????????"nanosecond?timeout?value?out?of?range");
????????}
????????if?(nanos?>=?500000?||?(nanos?!=?0?&&?timeout?==?0))?{
????????????timeout++;
????????}
????????wait(timeout);
????}
從源碼來看,JDK8中對納秒的處理,只做了四舍五入,所以還是按照毫秒來處理的,可能在未來的某個時間點會用到納秒級別的精度。雖然JDK提供了這三個版本,其實最后都是調用`wait(long timeout)`方法來實現的,wait()方法與wait(0)等效,而`wait(long timeout,int nanos)`從上面的源碼可以看到也是通過`wait(long timeout)`來完成的。下面我們通過一個簡單的例子來演示wait()方法的使用:
package?com.paddx.test.concurrent;
public?class?WaitTest?{
????public?void?testWait(){
????????System.out.println("Start-----");
????????try?{
????????????wait(1000);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????System.out.println("End-------");
????}
????public?static?void?main(String[]?args)?{
????????final?WaitTest?test?=?new?WaitTest();
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????test.testWait();
????????????}
????????}).start();
????}
}
這段代碼的意圖很簡單,就是程序執行以后,讓其暫停一秒,然后再執行。運行上述代碼,查看結果:
Start-----
Exception?in?thread?"Thread-0"?java.lang.IllegalMonitorStateException
????at?java.lang.Object.wait(Native?Method)
????at?com.paddx.test.concurrent.WaitTest.testWait(WaitTest.java:8)
????at?com.paddx.test.concurrent.WaitTest$1.run(WaitTest.java:20)
????at?java.lang.Thread.run(Thread.java:745)
這段程序并沒有按我們的預期輸出相應結果,而是拋出了一個異常。大家可能會覺得奇怪為什么會拋出異常?而拋出的IllegalMonitorStateException
異常又是什么?我們可以看一下JDK中對IllegalMonitorStateException
的描述:
Thrown?to?indicate?that?a?thread?has?attempted?to?wait?on?an?object's?monitor?or?to?notify?other?threads?waiting?on?an?object's?monitor?without?owning?the?specified?monitor.
這句話的意思大概就是:線程試圖等待對象的監視器或者試圖通知其他正在等待對象監視器的線程,但本身沒有對應的監視器的所有權。其實這個問題在《Java并發編程:Synchronized及其實現原理》一文中有提到過,wait方法是一個本地方法,其底層是通過一個叫做監視器鎖的對象來完成的。所以上面之所以會拋出異常,是因為在調用wait方式時沒有獲取到monitor對象的所有權,那如何獲取monitor對象所有權?Java中只能通過Synchronized關鍵字來完成,修改上述代碼,增加Synchronized關鍵字:
package?com.paddx.test.concurrent;
public?class?WaitTest?{
????public?synchronized?void?testWait(){//增加Synchronized關鍵字
????????System.out.println("Start-----");
????????try?{
????????????wait(1000);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????System.out.println("End-------");
????}
????public?static?void?main(String[]?args)?{
????????final?WaitTest?test?=?new?WaitTest();
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????test.testWait();
????????????}
????????}).start();
????}
}
現在再運行上述代碼,就能看到預期的效果了:
Start-----
End-------
所以,通過這個例子,大家應該很清楚,wait方法的使用必須在同步的范圍內,否則就會拋出IllegalMonitorStateException
異常,wait
方法的作用就是阻塞當前線程等待notify/notifyAll
方法的喚醒,或等待超時后自動喚醒。
2、notify/notifyAll方法

有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對象的monitor對象來實現的,所以只要在同一對象上去調用notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。notify和notifyAll的區別在于前者只能喚醒monitor上的一個線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程,看下面的例子很容易理解這兩者的差別:
package?com.paddx.test.concurrent;
public?class?NotifyTest?{
????public?synchronized?void?testWait(){
????????System.out.println(Thread.currentThread().getName()?+"?Start-----");
????????try?{
????????????wait(0);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????System.out.println(Thread.currentThread().getName()?+"?End-------");
????}
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????final?NotifyTest?test?=?new?NotifyTest();
????????for(int?i=0;i<5;i++)?{
????????????new?Thread(new?Runnable()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????test.testWait();
????????????????}
????????????}).start();
????????}
????????synchronized?(test)?{
????????????test.notify();
????????}
????????Thread.sleep(3000);
????????System.out.println("-----------分割線-------------");
????????synchronized?(test)?{
????????????test.notifyAll();
????????}
????}
}
輸出結果如下:
Thread-0?Start-----
Thread-1?Start-----
Thread-2?Start-----
Thread-3?Start-----
Thread-4?Start-----
Thread-0?End-------
-----------分割線-------------
Thread-4?End-------
Thread-3?End-------
Thread-2?End-------
Thread-1?End-------
從結果可以看出:調用notify
方法時只有線程Thread-0
被喚醒,但是調用notifyAll
時,所有的線程都被喚醒了。
最后,有兩點點需要注意:
(1)調用wait方法后,線程是會釋放對monitor對象的所有權的。
(2)一個通過wait方法阻塞的線程,必須同時滿足以下兩個條件才能被真正執行:
- 線程需要被喚醒(超時喚醒或調用notify/notifyll)。
- 線程喚醒后需要競爭到鎖(monitor)。
三、sleep/yield/join方法解析
上面我們已經清楚了wait和notify方法的使用和原理,現在我們再來看另外一組線程間協作的方法。這組方法跟上面方法的最明顯區別是:這幾個方法都位于Thread類中,而上面三個方法都位于Object類中。至于為什么,大家可以先思考一下。現在我們逐個分析sleep/yield/join方法:
1、sleep
sleep方法的作用是讓當前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區別。最簡單的區別是,wait方法依賴于同步,而sleep方法可以直接調用。而更深層次的區別在于sleep方法只是暫時讓出CPU的執行權,并不釋放鎖。而wait方法則需要釋放鎖。
package?com.paddx.test.concurrent;
public?class?SleepTest?{
????public?synchronized?void?sleepMethod(){
????????System.out.println("Sleep?start-----");
????????try?{
????????????Thread.sleep(1000);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????System.out.println("Sleep?end-----");
????}
????public?synchronized?void?waitMethod(){
????????System.out.println("Wait?start-----");
????????synchronized?(this){
????????????try?{
????????????????wait(1000);
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????????System.out.println("Wait?end-----");
????}
????public?static?void?main(String[]?args)?{
????????final?SleepTest?test1?=?new?SleepTest();
????????for(int?i?=?0;i<3;i++){
????????????new?Thread(new?Runnable()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????test1.sleepMethod();
????????????????}
????????????}).start();
????????}
????????try?{
????????????Thread.sleep(10000);//暫停十秒,等上面程序執行完成
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????System.out.println("-----分割線-----");
????????final?SleepTest?test2?=?new?SleepTest();
????????for(int?i?=?0;i<3;i++){
????????????new?Thread(new?Runnable()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????test2.waitMethod();
????????????????}
????????????}).start();
????????}
????}
}
執行結果:
Sleep?start-----
Sleep?end-----
Sleep?start-----
Sleep?end-----
Sleep?start-----
Sleep?end-----
-----分割線-----
Wait?start-----
Wait?start-----
Wait?start-----
Wait?end-----
Wait?end-----
Wait?end-----
這個結果的區別很明顯,通過sleep方法實現的暫停,程序是順序進入同步塊的,只有當上一個線程執行完成的時候,下一個線程才能進入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進入的。而wait方法則不同,當調用wait方法后,當前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒后,需要競爭鎖,獲取到鎖之后再繼續執行。
2、yield方法 yield方法的作用是暫停當前線程,以便其他線程有機會執行,不過不能指定暫停的時間,并且也不能保證當前線程馬上停止。yield方法只是將Running狀態轉變為Runnable狀態。我們還是通過一個例子來演示其使用:
package?com.paddx.test.concurrent;
public?class?YieldTest?implements?Runnable?{
????@Override
????public?void?run()?{
????????try?{
????????????Thread.sleep(100);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????for(int?i=0;i<5;i++){
????????????System.out.println(Thread.currentThread().getName()?+?":?"?+?i);
????????????Thread.yield();
????????}
????}
????public?static?void?main(String[]?args)?{
????????YieldTest?runn?=?new?YieldTest();
????????Thread?t1?=?new?Thread(runn,"FirstThread");
????????Thread?t2?=?new?Thread(runn,"SecondThread");
????????t1.start();
????????t2.start();
????}
}
運行結果如下:
FirstThread:?0
SecondThread:?0
FirstThread:?1
SecondThread:?1
FirstThread:?2
SecondThread:?2
FirstThread:?3
SecondThread:?3
FirstThread:?4
SecondThread:?4
這個例子就是通過yield方法來實現兩個線程的交替執行。不過請注意:這種交替并不一定能得到保證,源碼中也對這個問題進行說明:
/**
?????*?A?hint?to?the?scheduler?that?the?current?thread?is?willing?to?yield
?????*?its?current?use?of?a?processor.?The?scheduler?is?free?to?ignore?this
?????*?hint.
?????*
?????*?
?Yield?is?a?heuristic?attempt?to?improve?relative?progression
?????*?between?threads?that?would?otherwise?over-utilise?a?CPU.?Its?use
?????*?should?be?combined?with?detailed?profiling?and?benchmarking?to
?????*?ensure?that?it?actually?has?the?desired?effect.
?????*
?????*?
?It?is?rarely?appropriate?to?use?this?method.?It?may?be?useful
?????*?for?debugging?or?testing?purposes,?where?it?may?help?to?reproduce
?????*?bugs?due?to?race?conditions.?It?may?also?be?useful?when?designing
?????*?concurrency?control?constructs?such?as?the?ones?in?the
?????*?{@link?java.util.concurrent.locks}?package.
*/
這段話主要說明了三個問題:
- 調度器可能會忽略該方法。
- 使用的時候要仔細分析和測試,確保能達到預期的效果。
- 很少有場景要用到該方法,主要使用的地方是調試和測試。
3、join方法

join方法的作用是父線程等待子線程執行完成后再執行,換句話說就是將異步執行的線程合并為同步的線程。JDK中提供三個版本的join方法,其實現與wait方法類似,join()方法實際上執行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實現方式一致,暫時對納秒的支持也是不完整的。我們可以看下join方法的源碼,這樣更容易理解:
public?final?void?join()?throws?InterruptedException?{
????????join(0);
????}
?public?final?synchronized?void?join(long?millis)throws?InterruptedException?{
????????long?base?=?System.currentTimeMillis();
????????long?now?=?0;
????????if?(millis?0)?{
????????????throw?new?IllegalArgumentException("timeout?value?is?negative");
????????}
????????if?(millis?==?0)?{
????????????while?(isAlive())?{
????????????????wait(0);
????????????}
????????}?else?{
????????????while?(isAlive())?{
????????????????long?delay?=?millis?-?now;
????????????????if?(delay?<=?0)?{
????????????????????break;
????????????????}
????????????????wait(delay);
????????????????now?=?System.currentTimeMillis()?-?base;
????????????}
????????}
????}
public?final?synchronized?void?join(long?millis,?int?nanos)throws?InterruptedException?{
????????if?(millis?0)?{
????????????throw?new?IllegalArgumentException("timeout?value?is?negative");
????????}
????????if?(nanos?0?||?nanos?>?999999)?{
????????????throw?new?IllegalArgumentException(
????????????????????????????????"nanosecond?timeout?value?out?of?range");
????????}
????????if?(nanos?>=?500000?||?(nanos?!=?0?&&?millis?==?0))?{
????????????millis++;
????????}
????????join(millis);
????}
大家重點關注一下join(long millis)方法的實現,可以看出join方法就是通過wait方法來將線程的阻塞,如果join的線程還在執行,則將當前線程阻塞起來,直到join的線程執行完成,當前線程才能執行。不過有一點需要注意,這里的join只調用了wait方法,卻沒有對應的notify方法,原因是Thread的start方法中做了相應的處理,所以當join的線程執行完成以后,會自動喚醒主線程繼續往下執行。下面我們通過一個例子來演示join方法的作用:
(1)不使用join方法:
package?com.paddx.test.concurrent;
public?class?JoinTest?implements?Runnable{
????@Override
????public?void?run()?{
????????try?{
????????????System.out.println(Thread.currentThread().getName()?+?"?start-----");
????????????Thread.sleep(1000);
????????????System.out.println(Thread.currentThread().getName()?+?"?end------");
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????}
????public?static?void?main(String[]?args)?{
????????for?(int?i=0;i<5;i++)?{
????????????Thread?test?=?new?Thread(new?JoinTest());
????????????test.start();
????????}
????????System.out.println("Finished~~~");
????}
}
執行結果如下:
Thread-0?start-----
Thread-1?start-----
Thread-2?start-----
Thread-3?start-----
Finished~~~
Thread-4?start-----
Thread-2?end------
Thread-4?end------
Thread-1?end------
Thread-0?end------
Thread-3?end------
(2)使用join方法:
package?com.paddx.test.concurrent;
public?class?JoinTest?implements?Runnable{
????@Override
????public?void?run()?{
????????try?{
????????????System.out.println(Thread.currentThread().getName()?+?"?start-----");
????????????Thread.sleep(1000);
????????????System.out.println(Thread.currentThread().getName()?+?"?end------");
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????}
????public?static?void?main(String[]?args)?{
????????for?(int?i=0;i<5;i++)?{
????????????Thread?test?=?new?Thread(new?JoinTest());
????????????test.start();
????????????try?{
????????????????test.join();?//調用join方法
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????????System.out.println("Finished~~~");
????}
}
執行結果如下:
Thread-0?start-----
Thread-0?end------
Thread-1?start-----
Thread-1?end------
Thread-2?start-----
Thread-2?end------
Thread-3?start-----
Thread-3?end------
Thread-4?start-----
Thread-4?end------
Finished~~~
對比兩段代碼的執行結果很容易發現,在沒有使用join方法之間,線程是并發執行的,而使用join方法后,所有線程是順序執行的。
四、總結
本文主要詳細講解了wait/notify/notifyAll和sleep/yield/join
方法。最后回答一下上面提出的問題:wait/notify/notifyAll方法的作用是實現線程間的協作,那為什么這三個方法不是位于Thread類中,而是位于Object類中?位于Object中,也就相當于所有類都包含這三個方法(因為Java中所有的類都繼承自Object類)。要回答這個問題,還是得回過來看wait方法的實現原理,大家需要明白的是,wait等待的到底是什么東西?如果對上面內容理解的比較好的話,我相信大家應該很容易知道wait等待其實是對象monitor,由于Java中的每一個對象都有一個內置的monitor對象,自然所有的類都理應有wait/notify方法。
相關文章
Java 并發編程:核心理論
Java并發編程:Synchronized及其實現原理
Java并發編程:Synchronized底層優化(輕量級鎖、偏向鎖)
文末
文章收錄至 Github:?https://github.com/CoderMerlin/coder-programmingGitee:?https://gitee.com/573059382/coder-programming歡迎關注并star~

? ? ? ? ? ? ? ? ? 我知道你 “在看”