Java并發程式設計系列:
- Java 并發程式設計:核心理論
- Java并發程式設計:Synchronized及其實作原理
- Java并發程式設計:Synchronized底層優化(輕量級鎖、偏向鎖)
- Java 并發程式設計:線程間的協作(wait/notify/sleep/yield/join)
- Java 并發程式設計:volatile的使用及其原理
一、線程的狀态
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方法:
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中一共提供了這三個版本的方法,
(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方法
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對象來實作的,是以隻要在同一對象上去調用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方法來實作兩個線程的交替執行。不過請注意:這種交替并不一定能得到保證,源碼中也對這個問題進行說明:
|
這段話主要說明了三個問題:
- 排程器可能會忽略該方法。
- 使用的時候要仔細分析和測試,確定能達到預期的效果。
- 很少有場景要用到該方法,主要使用的地方是調試和測試。
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方法的作用是父線程等待子線程執行完成後再執行,換句話說就是将異步執行的線程合并為同步的線程。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~~~");
}
}
執行結果如下:
|
(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();
}
System.out.println("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方法。
作者:liuxiaopeng
部落格位址:http://www.cnblogs.com/paddix/
聲明:轉載請在文章頁面明顯位置給出原文連接配接
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);
}
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)
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.
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-------
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-------
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-----
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
|
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);
}
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~~~");
}
}
|
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~~~");
}
}
|