Java并發(fā)編程系列:
一、線程的狀態(tài)
Java中線程中狀態(tài)可分為五種:New(新建狀態(tài)),Runnable(就緒狀態(tài)),Running(運行狀態(tài)),Blocked(阻塞狀態(tài)),Dead(死亡狀態(tài))。
New:新建狀態(tài),當線程創(chuàng)建完成時為新建狀態(tài),即new Thread(...),還沒有調(diào)用start方法時,線程處于新建狀態(tài)。
Runnable:就緒狀態(tài),當調(diào)用線程的的start方法后,線程進入就緒狀態(tài),等待CPU資源。處于就緒狀態(tài)的線程由Java運行時系統(tǒng)的線程調(diào)度程序(thread scheduler)來調(diào)度。
Running:運行狀態(tài),就緒狀態(tài)的線程獲取到CPU執(zhí)行權(quán)以后進入運行狀態(tài),開始執(zhí)行run方法。
Blocked:阻塞狀態(tài),線程沒有執(zhí)行完,由于某種原因(如,I/O操作等)讓出CPU執(zhí)行權(quán),自身進入阻塞狀態(tài)。
Dead:死亡狀態(tài),線程執(zhí)行完成或者執(zhí)行過程中出現(xiàn)異常,線程就會進入死亡狀態(tài)。
這五種狀態(tài)之間的轉(zhuǎn)換關(guān)系如下圖所示:
有了對這五種狀態(tài)的基本了解,現(xiàn)在我們來看看Java中是如何實現(xiàn)這幾種狀態(tài)的轉(zhuǎn)換的。
二、wait/notify/notifyAll方法的使用
1、wait方法:
void wait() | Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. |
void wait(long timeout) | Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed. |
void wait(long timeout, int nanos) | Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed. |
JDK中一共提供了這三個版本的方法,
?。?)wait()方法的作用是將當前運行的線程掛起(即讓其進入阻塞狀態(tài)),直到notify或notifyAll方法來喚醒線程.
(2)wait(long timeout),該方法與wait()方法類似,唯一的區(qū)別就是在指定時間內(nèi),如果沒有notify或notifAll方法的喚醒,也會自動喚醒。
(3)至于wait(long timeout,long nanos),本意在于更精確的控制調(diào)度時間,不過從目前版本來看,該方法貌似沒有完整的實現(xiàn)該功能,其源碼(JDK1.8)如下:
1 public final void wait(long timeout, int nanos) throws InterruptedException { 2 if (timeout < 0) { 3 throw new IllegalArgumentException("timeout value is negative"); 4 } 5 6 if (nanos < 0 || nanos > 999999) { 7 throw new IllegalArgumentException( 8 "nanosecond timeout value out of range"); 9 }10 11 if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {12 timeout++;13 }14 15 wait(timeout);16 }
從源碼來看,JDK8中對納秒的處理,只做了四舍五入,所以還是按照毫秒來處理的,可能在未來的某個時間點會用到納秒級別的精度。雖然JDK提供了這三個版本,其實最后都是調(diào)用wait(long timeout)方法來實現(xiàn)的,wait()方法與wait(0)等效,而wait(long timeout,int nanos)從上面的源碼可以看到也是通過wait(long timeout)來完成的。下面我們通過一個簡單的例子來演示wait()方法的使用:
1 package com.paddx.test.concurrent; 2 3 public class WaitTest { 4 5 public void testWait(){ 6 System.out.println("Start-----"); 7 try { 8 wait(1000); 9 } catch (InterruptedException e) {10 e.printStackTrace();11 }12 System.out.println("End-------");13 }14 15 public static void main(String[] args) {16 final WaitTest test = new WaitTest();17 new Thread(new Runnable() {18 @Override19 public void run() {20 test.testWait();21 }22 }).start();23 }24 }
這段代碼的意圖很簡單,就是程序執(zhí)行以后,讓其暫停一秒,然后再執(zhí)行。運行上述代碼,查看結(jié)果:
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 ) |
這段程序并沒有按我們的預期輸出相應結(jié)果,而是拋出了一個異常。大家可能會覺得奇怪為什么會拋出異常?而拋出的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. |
這句話的意思大概就是:線程試圖等待對象的監(jiān)視器或者試圖通知其他正在等待對象監(jiān)視器的線程,但本身沒有對應的監(jiān)視器的所有權(quán)。其實這個問題在《Java并發(fā)編程:Synchronized及其實現(xiàn)原理》一文中有提到過,wait方法是一個本地方法,其底層是通過一個叫做監(jiān)視器鎖的對象來完成的。所以上面之所以會拋出異常,是因為在調(diào)用wait方式時沒有獲取到monitor對象的所有權(quán),那如何獲取monitor對象所有權(quán)?Java中只能通過Synchronized關(guān)鍵字來完成,修改上述代碼,增加Synchronized關(guān)鍵字:
1 package com.paddx.test.concurrent; 2 3 public class WaitTest { 4 5 public synchronized void testWait(){//增加Synchronized關(guān)鍵字 6 System.out.println("Start-----"); 7 try { 8 wait(1000); 9 } catch (InterruptedException e) {10 e.printStackTrace();11 }12 System.out.println("End-------");13 }14 15 public static void main(String[] args) {16 final WaitTest test = new WaitTest();17 new Thread(new Runnable() {18 @Override19 public void run() {20 test.testWait();21 }22 }).start();23 }24 }
現(xiàn)在再運行上述代碼,就能看到預期的效果了:
Start----- End------- |
所以,通過這個例子,大家應該很清楚,wait方法的使用必須在同步的范圍內(nèi),否則就會拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當前線程等待notify/notifyAll方法的喚醒,或等待超時后自動喚醒。
2、notify/notifyAll方法
void notify() | Wakes up a single thread that is waiting on this object's monitor. |
void notifyAll() | Wakes up all threads that are waiting on this object's monitor. |
有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對象的monitor對象來實現(xiàn)的,所以只要在同一對象上去調(diào)用notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。notify和notifyAll的區(qū)別在于前者只能喚醒monitor上的一個線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程,看下面的例子很容易理解這兩者的差別:
1 package com.paddx.test.concurrent; 2 3 public class NotifyTest { 4 public synchronized void testWait(){ 5 System.out.println(Thread.currentThread().getName() +" Start-----"); 6 try { 7 wait(0); 8 } catch (InterruptedException e) { 9 e.printStackTrace();10 }11 System.out.println(Thread.currentThread().getName() +" End-------");12 }13 14 public static void main(String[] args) throws InterruptedException {15 final NotifyTest test = new NotifyTest();16 for(int i=0;i<5;i++) {17 new Thread(new Runnable() {18 @Override19 public void run() {20 test.testWait();21 }22 }).start();23 }24 25 synchronized (test) {26 test.notify();27 }28 Thread.sleep(3000);29 System.out.println("-----------分割線-------------");30 31 synchronized (test) {32 test.notifyAll();33 }34 }35 }
輸出結(jié)果如下:
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------- |
從結(jié)果可以看出:調(diào)用notify方法時只有線程Thread-0被喚醒,但是調(diào)用notifyAll時,所有的線程都被喚醒了。
最后,有兩點點需要注意:
?。?)調(diào)用wait方法后,線程是會釋放對monitor對象的所有權(quán)的。
?。?)一個通過wait方法阻塞的線程,必須同時滿足以下兩個條件才能被真正執(zhí)行:
三、sleep/yield/join方法解析
上面我們已經(jīng)清楚了wait和notify方法的使用和原理,現(xiàn)在我們再來看另外一組線程間協(xié)作的方法。這組方法跟上面方法的最明顯區(qū)別是:這幾個方法都位于Thread類中,而上面三個方法都位于Object類中。至于為什么,大家可以先思考一下?,F(xiàn)在我們逐個分析sleep/yield/join方法:
1、sleep
sleep方法的作用是讓當前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區(qū)別。最簡單的區(qū)別是,wait方法依賴于同步,而sleep方法可以直接調(diào)用。而更深層次的區(qū)別在于sleep方法只是暫時讓出CPU的執(zhí)行權(quán),并不釋放鎖。而wait方法則需要釋放鎖。
1 package com.paddx.test.concurrent; 2 3 public class SleepTest { 4 public synchronized void sleepMethod(){ 5 System.out.println("Sleep start-----"); 6 try { 7 Thread.sleep(1000); 8 } catch (InterruptedException e) { 9 e.printStackTrace();10 }11 System.out.println("Sleep end-----");12 }13 14 public synchronized void waitMethod(){15 System.out.println("Wait start-----");16 synchronized (this){17 try {18 wait(1000);19 } catch (InterruptedException e) {20 e.printStackTrace();21 }22 }23 System.out.println("Wait end-----");24 }25 26 public static void main(String[] args) {27 final SleepTest test1 = new SleepTest();28 29 for(int i = 0;i<3;i++){30 new Thread(new Runnable() {31 @Override32 public void run() {33 test1.sleepMethod();34 }35 }).start();36 }37 38 39 try {40 Thread.sleep(10000);//暫停十秒,等上面程序執(zhí)行完成41 } catch (InterruptedException e) {42 e.printStackTrace();43 }44 System.out.println("-----分割線-----");45 46 final SleepTest test2 = new SleepTest();47 48 for(int i = 0;i<3;i++){49 new Thread(new Runnable() {50 @Override51 public void run() {52 test2.waitMethod();53 }54 }).start();55 }56 57 }58 }
執(zhí)行結(jié)果:
Sleep start----- Sleep end----- Sleep start----- Sleep end----- Sleep start----- Sleep end----- -----分割線----- Wait start----- Wait start----- Wait start----- Wait end----- Wait end----- Wait end----- |
這個結(jié)果的區(qū)別很明顯,通過sleep方法實現(xiàn)的暫停,程序是順序進入同步塊的,只有當上一個線程執(zhí)行完成的時候,下一個線程才能進入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進入的。而wait方法則不同,當調(diào)用wait方法后,當前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒后,需要競爭鎖,獲取到鎖之后再繼續(xù)執(zhí)行。
2、yield方法
yield方法的作用是暫停當前線程,以便其他線程有機會執(zhí)行,不過不能指定暫停的時間,并且也不能保證當前線程馬上停止。yield方法只是將Running狀態(tài)轉(zhuǎn)變?yōu)镽unnable狀態(tài)。我們還是通過一個例子來演示其使用:
1 package com.paddx.test.concurrent; 2 3 public class YieldTest implements Runnable { 4 @Override 5 public void run() { 6 try { 7 Thread.sleep(100); 8 } catch (InterruptedException e) { 9 e.printStackTrace();10 }11 for(int i=0;i<5;i++){12 System.out.println(Thread.currentThread().getName() + ": " + i);13 Thread.yield();14 }15 }16 17 public static void main(String[] args) {18 YieldTest runn = new YieldTest();19 Thread t1 = new Thread(runn,"FirstThread");20 Thread t2 = new Thread(runn,"SecondThread");21 22 t1.start();23 t2.start();24 25 }26 }
運行結(jié)果如下:
FirstThread: 0 SecondThread: 0 FirstThread: 1 SecondThread: 1 FirstThread: 2 SecondThread: 2 FirstThread: 3 SecondThread: 3 FirstThread: 4 SecondThread: 4 |
這個例子就是通過yield方法來實現(xiàn)兩個線程的交替執(zhí)行。不過請注意:這種交替并不一定能得到保證,源碼中也對這個問題進行說明:
/** * 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. * * <p> 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. * * <p> 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方法
void join() | Waits for this thread to die. |
void join(long millis) | Waits at most millis milliseconds for this thread to die. |
void join(long millis, int nanos) | Waits at most millis milliseconds plus nanos nanoseconds for this thread to die. |
join方法的作用是父線程等待子線程執(zhí)行完成后再執(zhí)行,換句話說就是將異步執(zhí)行的線程合并為同步的線程。JDK中提供三個版本的join方法,其實現(xiàn)與wait方法類似,join()方法實際上執(zhí)行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實現(xiàn)方式一致,暫時對納秒的支持也是不完整的。我們可以看下join方法的源碼,這樣更容易理解:
1 public final void join() throws InterruptedException { 2 join(0); 3 } 4 5 public final synchronized void join(long millis) 6 throws InterruptedException { 7 long base = System.currentTimeMillis(); 8 long now = 0; 9 10 if (millis < 0) {11 throw new IllegalArgumentException("timeout value is negative");12 }13 14 if (millis == 0) {15 while (isAlive()) {16 wait(0);17 }18 } else {19 while (isAlive()) {20 long delay = millis - now;21 if (delay <= 0) {22 break;23 }24 wait(delay);25 now = System.currentTimeMillis() - base;26 }27 }28 }29 30 public final synchronized void join(long millis, int nanos)31 throws InterruptedException {32 33 if (millis < 0) {34 throw new IllegalArgumentException("timeout value is negative");35 }36 37 if (nanos < 0 || nanos > 999999) {38 throw new IllegalArgumentException(39 "nanosecond timeout value out of range");40 }41 42 if (nanos >= 500000 || (nanos != 0 && millis == 0)) {43 millis++;44 }45 46 join(millis);47 }
大家重點關(guān)注一下join(long millis)方法的實現(xiàn),可以看出join方法就是通過wait方法來將線程的阻塞,如果join的線程還在執(zhí)行,則將當前線程阻塞起來,直到join的線程執(zhí)行完成,當前線程才能執(zhí)行。不過有一點需要注意,這里的join只調(diào)用了wait方法,卻沒有對應的notify方法,原因是Thread的start方法中做了相應的處理,所以當join的線程執(zhí)行完成以后,會自動喚醒主線程繼續(xù)往下執(zhí)行。下面我們通過一個例子來演示join方法的作用:
(1)不使用join方法:
1 package com.paddx.test.concurrent; 2 3 public class JoinTest implements Runnable{ 4 @Override 5 public void run() { 6 7 try { 8 System.out.println(Thread.currentThread().getName() + " start-----"); 9 Thread.sleep(1000);10 System.out.println(Thread.currentThread().getName() + " end------");11 } catch (InterruptedException e) {12 e.printStackTrace();13 }14 }15 16 public static void main(String[] args) {17 for (int i=0;i<5;i++) {18 Thread test = new Thread(new JoinTest());19 test.start();20 }21 22 System.out.println("Finished~~~");23 }24 }
執(zhí)行結(jié)果如下:
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方法:
1 package com.paddx.test.concurrent; 2 3 public class JoinTest implements Runnable{ 4 @Override 5 public void run() { 6 7 try { 8 System.out.println(Thread.currentThread().getName() + " start-----"); 9 Thread.sleep(1000);10 System.out.println(Thread.currentThread().getName() + " end------");11 } catch (InterruptedException e) {12 e.printStackTrace();13 }14 }15 16 public static void main(String[] args) {17 for (int i=0;i<5;i++) {18 Thread test = new Thread(new JoinTest());19 test.start();20 try {21 test.join(); //調(diào)用join方法22 } catch (InterruptedException e) {23 e.printStackTrace();24 }25 }26 27 System.out.println("Finished~~~");28 }29 }
執(zhí)行結(jié)果如下:
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~~~ |
對比兩段代碼的執(zhí)行結(jié)果很容易發(fā)現(xiàn),在沒有使用join方法之間,線程是并發(fā)執(zhí)行的,而使用join方法后,所有線程是順序執(zhí)行的。
四、總結(jié)
本文主要詳細講解了wait/notify/notifyAll和sleep/yield/join方法。最后回答一下上面提出的問題:wait/notify/notifyAll方法的作用是實現(xiàn)線程間的協(xié)作,那為什么這三個方法不是位于Thread類中,而是位于Object類中?位于Object中,也就相當于所有類都包含這三個方法(因為Java中所有的類都繼承自Object類)。要回答這個問題,還是得回過來看wait方法的實現(xiàn)原理,大家需要明白的是,wait等待的到底是什么東西?如果對上面內(nèi)容理解的比較好的話,我相信大家應該很容易知道wait等待其實是對象monitor,由于Java中的每一個對象都有一個內(nèi)置的monitor對象,自然所有的類都理應有wait/notify方法。