本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。
方法簡介
wait方法
當一個線程調用一個共享變量的wait()方法時,該調用線程會被阻塞掛起(進入waiting狀態),直到發生下面幾件事情之一才能返回:
- 其他線程調用了該共享對象的notify()或者notifyAll()方法;
- 其他線程調用了該線程的interrupt()方法,該線程拋出InterruptedException異常返回。
另外需要注意的是,如果調用wait()方法的線程沒有事先獲取該對象的監視器鎖,則調用wait()方法時調用線程會拋出IllegalMonitorStateException異常。如果當前線程已經獲取了鎖資源,調用wait方法之后會釋放這個鎖資源,但是只會釋放當前共享變量上的鎖,如果當前線程還持有其他共享變量的鎖,則這些鎖是不會被釋放的。
wait方法還有一個重載方法wait(long time),這個方法會等待time時間,如果在這個時間內沒有其他線程來喚醒它的話,這個線程會自己喚醒繼續獲得執行機會。
notify方法
notify方法會喚醒等待對象監視器的單個線程,如果等待對象監視器的有多個線程,則選取其中一個線程進行喚醒,到底選擇喚醒哪個線程是任意的,由CPU自己決定。如果沒有再調用notify方法,其他阻塞的線程可能就永遠得不到再執行的機會了。
此外,被喚醒的線程不能馬上從wait方法返回並繼續執行,它必須在獲取了共享對象的監視器鎖后才可以返回,也就是喚醒它的線程釋放了共享變量上的監視器鎖后,被喚醒的線程也不一定會獲取到共享對象的監視器鎖,這是因為該線程還需要和其他線程一起競爭該鎖,只有該線程競爭到了共享變量的監視器鎖后才可以繼續執行。
一個還需要注意的地方是,在共享變量上調用notifyAll()方法只會喚醒調用這個方法前調用了wait系列函數而被放入共享變量等待集合里面的線程。如果調用notifyAll()方法后一個線程調用了該共享變量的wait()方法而被放入阻塞集合,則該線程是不會被喚醒的
類似wait系列方法,只有當前線程獲取到了共享變量的監視器鎖后,才可以調用共享變量的notify()方法,否則會拋出IllegalMonitorStateException異常。
notify方法還有個兄弟方法notifyAll,這個方法會喚醒所有等待監視器對象的線程。
wait-notify模式的典型應用
wait-notify模式的一個典型應用就是可以實現生產者-消費者模式。讓我印象很深是我畢業那年阿里巴巴校園招聘的一個筆試題:
有一個蘋果箱,有10個人向這個箱子中每次隨機放入一個蘋果,有10個人每次隨機從這個箱子中隨機拿走一個蘋果,同時需要滿足箱子中的蘋果總數不能超過50個。請用代碼實現上面的場景(不能使用並發集合框架)
現在看來,這道題不就是為wait-notify模式量身打造的一道題目么。當時水平有限,又急急忙忙的,所以記得當時寫的不太好。這邊重新整理下這個代碼
public class AppleBox {
private int appleCount;
public synchronized void putApple() {
while (appleCount >= 50) {
try {
//會釋放鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
appleCount++;
String name = Thread.currentThread().getName();
System.out.println("[" + name + "]放入一個,當前盒子中蘋果數:" + appleCount);
this.notifyAll();
}
public synchronized void takeApple() {
while (appleCount <= 0) {
try {
//會釋放鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
appleCount--;
String name = Thread.currentThread().getName();
System.out.println("[" + name + "]拿走一個,當前盒子中蘋果數:" + appleCount);
this.notifyAll();
}
private static class AppleTaker implements Runnable {
private AppleBox appleBox;
public AppleTaker(AppleBox appleBox) {
this.appleBox = appleBox;
}
@Override
public void run() {
while (true) {
appleBox.takeApple();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class ApplePutter implements Runnable {
private AppleBox appleBox;
public ApplePutter(AppleBox appleBox) {
this.appleBox = appleBox;
}
@Override
public void run() {
while (true) {
appleBox.putApple();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
AppleBox appleBox = new AppleBox();
for (int i = 0; i < 20; i++) {
Thread t = new Thread(new ApplePutter(appleBox));
t.setName("ApplePutter:" + i);
t.start();
}
for (int i = 0; i < 20; i++) {
Thread t = new Thread(new AppleTaker(appleBox));
t.setName("AppleTaker:" + i);
t.start();
}
}
}
執行結果如下:
[ApplePutter:0]放入一個,當前盒子中蘋果數:1
[ApplePutter:1]放入一個,當前盒子中蘋果數:2
[ApplePutter:5]放入一個,當前盒子中蘋果數:3
[ApplePutter:9]放入一個,當前盒子中蘋果數:4
[ApplePutter:13]放入一個,當前盒子中蘋果數:5
[ApplePutter:2]放入一個,當前盒子中蘋果數:6
[ApplePutter:6]放入一個,當前盒子中蘋果數:7
[ApplePutter:10]放入一個,當前盒子中蘋果數:8
[ApplePutter:17]放入一個,當前盒子中蘋果數:9
[ApplePutter:14]放入一個,當前盒子中蘋果數:10
[ApplePutter:18]放入一個,當前盒子中蘋果數:11
[ApplePutter:3]放入一個,當前盒子中蘋果數:12
[ApplePutter:7]放入一個,當前盒子中蘋果數:13
[ApplePutter:11]放入一個,當前盒子中蘋果數:14
[ApplePutter:8]放入一個,當前盒子中蘋果數:15
[ApplePutter:15]放入一個,當前盒子中蘋果數:16
[ApplePutter:19]放入一個,當前盒子中蘋果數:17
[ApplePutter:4]放入一個,當前盒子中蘋果數:18
[AppleTaker:3]拿走一個,當前盒子中蘋果數:17
[ApplePutter:12]放入一個,當前盒子中蘋果數:18
[AppleTaker:1]拿走一個,當前盒子中蘋果數:17
[AppleTaker:5]拿走一個,當前盒子中蘋果數:16
[ApplePutter:16]放入一個,當前盒子中蘋果數:17
[AppleTaker:0]拿走一個,當前盒子中蘋果數:16
[AppleTaker:12]拿走一個,當前盒子中蘋果數:15
[AppleTaker:8]拿走一個,當前盒子中蘋果數:14
[AppleTaker:16]拿走一個,當前盒子中蘋果數:13
[AppleTaker:7]拿走一個,當前盒子中蘋果數:12
[AppleTaker:11]拿走一個,當前盒子中蘋果數:11
[AppleTaker:19]拿走一個,當前盒子中蘋果數:10
[AppleTaker:9]拿走一個,當前盒子中蘋果數:9
[AppleTaker:13]拿走一個,當前盒子中蘋果數:8
[AppleTaker:2]拿走一個,當前盒子中蘋果數:7
[AppleTaker:6]拿走一個,當前盒子中蘋果數:6
[AppleTaker:10]拿走一個,當前盒子中蘋果數:5
[AppleTaker:14]拿走一個,當前盒子中蘋果數:4
[AppleTaker:4]拿走一個,當前盒子中蘋果數:3
[AppleTaker:15]拿走一個,當前盒子中蘋果數:2
[AppleTaker:18]拿走一個,當前盒子中蘋果數:1
[AppleTaker:17]拿走一個,當前盒子中蘋果數:0
[ApplePutter:0]放入一個,當前盒子中蘋果數:1
[ApplePutter:1]放入一個,當前盒子中蘋果數:2
[ApplePutter:5]放入一個,當前盒子中蘋果數:3
[ApplePutter:9]放入一個,當前盒子中蘋果數:4
[ApplePutter:13]放入一個,當前盒子中蘋果數:5
[ApplePutter:17]放入一個,當前盒子中蘋果數:6
[ApplePutter:2]放入一個,當前盒子中蘋果數:7
[ApplePutter:6]放入一個,當前盒子中蘋果數:8
[ApplePutter:10]放入一個,當前盒子中蘋果數:9
[ApplePutter:14]放入一個,當前盒子中蘋果數:10
[ApplePutter:18]放入一個,當前盒子中蘋果數:11
[ApplePutter:3]放入一個,當前盒子中蘋果數:12
[ApplePutter:7]放入一個,當前盒子中蘋果數:13
[ApplePutter:11]放入一個,當前盒子中蘋果數:14
[ApplePutter:15]放入一個,當前盒子中蘋果數:15
[ApplePutter:19]放入一個,當前盒子中蘋果數:16
[AppleTaker:3]拿走一個,當前盒子中蘋果數:15
[ApplePutter:4]放入一個,當前盒子中蘋果數:16
[ApplePutter:8]放入一個,當前盒子中蘋果數:17
[ApplePutter:12]放入一個,當前盒子中蘋果數:18
**PS: 多線程編程中,最要的重要的兩點是先抽象出共享變量是什么,任務類(Runner)是什么 **
wait-notify模式的經典寫法
生產者和消費者的邏輯都可以統一抽象成以下幾個步驟:
- step1:獲得對象的鎖;
- step2:循環判斷是否需要進行生產活動,如果不需要進行生產就調用wait方法,暫停當前線程;如果需要進行生產活動,進行對應的生產活動;
- step3:通知等待線程
偽代碼如下:
synchronized(對象) {
//這邊進行循環判斷的原因是為了防止偽喚醒,也就是不是消費線程或者生產線程調用notify方法將waiting線程喚醒的
while(條件){
對象.wait();
}
//進行生產或者消費活動
doSomething();
對象.notifyAll();
}