線程通信的方法
程序在使用多線程執(zhí)行任務(wù)時,經(jīng)常需要線程之間協(xié)同工作。此時,我們需要了解線程通信的手段。
線程通信大致分為以下四類:
- 文件共享
- 網(wǎng)絡(luò)共享
- 共享變量
- JDK提供的線程協(xié)調(diào)API
本文主要研究第四類,如何使用JDK提供的API正確地阻塞、喚醒目標(biāo)線程。
Thread#suspend()和Thread#resume()
Thread#suspend():掛起目標(biāo)線程不再繼續(xù)執(zhí)行,直到它被resume()喚醒。
Thread#resume():如果目標(biāo)線程已被掛起,那么喚醒目標(biāo)線程并允許它繼續(xù)執(zhí)行。
舉個栗子:
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
// 正常的suspend/resume
private static void suspendResume() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
System.out.println("沒有包子,進(jìn)入等待");
// 掛起當(dāng)前線程
Thread.currentThread().suspend();
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
System.out.println("包子做好了");
// 喚醒目標(biāo)線程
thread.resume();
thread.join();
}
public static void main(String[] args) throws InterruptedException {
suspendResume();
}
}
運(yùn)行main()方法,控制臺輸出如下:
沒有包子,進(jìn)入等待
包子做好了
買到包子,回家
suspend()和resume()幫助我們在適當(dāng)?shù)臅r候阻塞和喚醒線程,成功地模擬了顧客在包子店買包子的情景。
但是,這對API在JDK1.2之后被棄用,原因是它們特別容易形成死鎖。
如果目標(biāo)線程持有監(jiān)視器鎖,在調(diào)用suspend()掛起目標(biāo)線程時并不會釋放這把鎖。此時,如果其他線程在調(diào)用目標(biāo)線程的resume()方法之前也要先獲取這把鎖,死鎖就產(chǎn)生了。
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
// 會死鎖的suspend/resume
private static void suspendResumeDeadLock() throws InterruptedException {
final Object lock = new Object();
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
System.out.println("沒有包子,進(jìn)入等待");
// 當(dāng)前線程拿到鎖,然后掛起
synchronized (lock) {
Thread.currentThread().suspend();
}
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
// 爭取到鎖以后再喚醒目標(biāo)線程
synchronized (lock) { // 此處會一直BLOCKED
System.out.println("包子做好了");
thread.resume();
}
thread.join();
}
public static void main(String[] args) throws InterruptedException {
suspendResumeDeadLock();
}
}
運(yùn)行main()方法,程序無法退出,控制臺輸出如下:
沒有包子,進(jìn)入等待
喚醒目標(biāo)線程的條件已經(jīng)滿足(包子做好了),但是拿不到鎖,就無法喚醒目標(biāo)線程,程序就這樣被“凍結(jié)”。
即使在不涉及監(jiān)視器鎖的情況下使用suspend()和resume(),如果調(diào)用順序不當(dāng),線程也可能永遠(yuǎn)掛起。
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
// 會永遠(yuǎn)掛起的suspend/resume
private static void suspendForever() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
try {
// 睡眠一段時間,讓resume()方法先執(zhí)行
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("沒有包子,進(jìn)入等待");
// 掛起當(dāng)前線程
Thread.currentThread().suspend();
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
System.out.println("包子做好了");
// 喚醒目標(biāo)線程(此時線程還未掛起)
thread.resume();
thread.join();
}
public static void main(String[] args) throws InterruptedException {
suspendForever();
}
}
運(yùn)行main()方法,程序無法退出,控制臺輸出如下:
包子做好了
沒有包子,進(jìn)入等待
上面的例子中,resume()在suspend()之前被調(diào)用,目標(biāo)線程被掛起之后,沒有線程來調(diào)用resume()喚醒它,就會永遠(yuǎn)掛起。
既然Thread#suspend()和Thread#resume()已經(jīng)被棄用,那就讓我們來看看它們的替代方法吧。
Object#wait()和Object#notify()/Object#notifyAll()
調(diào)用以下方法時,當(dāng)前線程必須是對象監(jiān)視器鎖的持有者。
Object#wait():將當(dāng)前線程放入對象監(jiān)視器鎖的等待集合,釋放對象的監(jiān)視器鎖,線程不再被操作系統(tǒng)調(diào)度,進(jìn)入等待直到被喚醒或中斷,然后再次競爭獲得對象的監(jiān)視器鎖,恢復(fù)執(zhí)行。
Object#notify():喚醒對象監(jiān)視器鎖等待集合中的任意一個線程。
Object#notifyAll():喚醒對象監(jiān)視器鎖等待集合中的所有線程。
因此,wait()/notify()只適用于線程持有鎖的情境下。
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
private static void waitNotify() throws InterruptedException {
final Object lock = new Object();
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
synchronized (lock) {
try {
System.out.println("沒有包子,進(jìn)入等待");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
// 通知目標(biāo)線程
synchronized (lock) {
System.out.println("包子做好了");
lock.notify();
}
thread.join();
}
public static void main(String[] args) throws InterruptedException {
waitNotify();
}
}
運(yùn)行main()方法,控制臺輸出如下:
沒有包子,進(jìn)入等待
包子做好了
買到包子,回家
wait()/notify幫助我們成功地買到了包子。
與suspend()/resume()相似,wait()/notify()如果調(diào)用順序不當(dāng),也會造成線程無法被喚醒:
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
private static void waitForever() throws InterruptedException {
final Object lock = new Object();
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
try {
// 睡眠一段時間,讓notify()先執(zhí)行
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
try {
System.out.println("沒有包子,進(jìn)入等待");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
// 通知目標(biāo)線程
synchronized (lock) {
System.out.println("包子做好了");
lock.notify();
}
thread.join();
}
public static void main(String[] args) throws InterruptedException {
waitForever();
}
}
運(yùn)行main()方法,程序無法退出,控制臺輸出如下:
包子做好了
沒有包子,進(jìn)入等待
目標(biāo)線程的notify()方法先于wait()方法被調(diào)用,導(dǎo)致wait()方法被調(diào)用后沒有線程來喚醒它,就會一直處于WAITING狀態(tài)。
LockSupport#park()和LockSupport#unpark()
與wait()/notify()不同,park()/unpark()調(diào)用時線程不用獲取對象的監(jiān)視器鎖。
LockSupport#park():禁止當(dāng)前線程進(jìn)行線程調(diào)度,直到許可證可用。如果當(dāng)前許可證可用,那么消費(fèi)該許可證,本地調(diào)用立刻返回。
LockSupport#unpark():如果目標(biāo)線程許可證不可用,則為其提供許可證。否則,目標(biāo)線程的下一次park()調(diào)用不會阻塞。
可以看出,park()與unpark()為線程維護(hù)了一個許可證,該許可證只有可用/不可用兩種狀態(tài)(默認(rèn)不可用)。unpark()將許可證置為可用。而park()在許可證不可用的時候阻塞,在許可證可用的時候消費(fèi)該許可證(將其置為不可用)然后立即返回。因此,park()能否從阻塞中恢復(fù)與park()/unpark()的調(diào)用順序無關(guān)。
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
private static void parkUnpark() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
System.out.println("沒有包子,進(jìn)入等待");
// 掛起當(dāng)前線程
LockSupport.park();
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
System.out.println("包子做好了");
// 喚醒目標(biāo)線程
LockSupport.unpark(thread);
thread.join();
}
public static void main(String[] args) throws InterruptedException {
parkUnpark();
}
}
運(yùn)行main()方法,控制臺輸出如下:
沒有包子,進(jìn)入等待
包子做好了
買到包子,回家
雖然park()/unpark()沒有調(diào)用順序的要求,但是如果在調(diào)用park()時,當(dāng)前線程已經(jīng)持有了對象的監(jiān)視器鎖,park()不會釋放該鎖。此時,如果在調(diào)用unpark()前也需要獲取對象的監(jiān)視器鎖,就會死鎖。
public class ThreadCommunication {
private static volatile Object steamedStuffedBun;
private static void parkUnparkDeadLock() throws InterruptedException {
final Object lock = new Object();
Thread thread = new Thread(new Runnable() {
public void run() {
while (steamedStuffedBun == null) {
synchronized (lock) {
System.out.println("沒有包子,進(jìn)入等待");
// 掛起當(dāng)前線程,但是不會釋放鎖
LockSupport.park();
}
}
System.out.println("買到包子,回家");
}
});
thread.start();
// 3s后生產(chǎn)一個包子
Thread.sleep(3000);
steamedStuffedBun = new Object();
// 先拿到鎖,再喚醒目標(biāo)線程
// 但是鎖現(xiàn)在被park()線程持有,此處一直BLOCKED,死鎖了
synchronized (lock) {
System.out.println("包子做好了");
LockSupport.unpark(thread);
}
thread.join();
}
public static void main(String[] args) throws InterruptedException {
parkUnparkDeadLock();
}
}
注意:
wait()和park()都有可能被偽喚醒,建議在循環(huán)中檢查等待/掛起條件,防止程序在不滿足結(jié)束條件的情況下退出。