文章目錄
1 wait、notify、notifyAll簡單介紹
1.1 使用方法 + 爲什麼不是Thread類的方法
爲什麼不是Thread類的方法
首先應該明確wait、notify、notifyAll三個方法都是對鎖對象的操作
,而鎖可以是任何對象。在java的世界中任何對象都屬於Object類,因此這三個方法都是Object的方法
, 而不是線程對象Thread的方法。
使用方法
需要注意兩點:
- (1)這三個方法必須在synchronized關鍵字包含的
臨界區
(簡單理解,就是代碼塊)內使用 - (2)使用方式爲
鎖對象.方法()
,比如obj.wait();
1.2 什麼時候加鎖、什麼時候釋放鎖?
必須要明確以下幾點:
- (1)
notify和notifyAll方法不會釋放鎖
,這兩個方法只是通知其他使用該鎖當鎖
但是在wait狀態的線程,可以準備搶鎖了
- 這裏還要格外注意一點,其他使用該鎖當鎖且處於wait狀態的線程只有被notify或notifyAll喚醒了,纔有資格搶鎖
- (2)某個鎖對象調用wait方法會立即釋放
當前線程的該對象鎖
, 且其他線程通過notify/notifyAll方法通知該線程可以搶該對象鎖時,如果當前線程搶到了,會從當前鎖的wait方法之後開始執行
— 即從哪裏wait,從哪裏執行;
- (3)在synchronized、wait、notify、notifyAll的組合裏
加鎖的方式只有一個
即進入同步代碼塊時加鎖;- 正常情況下釋放鎖的方式有兩個: ①鎖對象調用wait方法時會釋放鎖 ;② 走完同步代碼塊時自動釋放鎖
- 還有一種釋放鎖的時機: 同步代碼塊裏出現異常。
1.3 notify、notifyAll的區別
- 某個鎖對象的notify只會喚醒
一個
使用該鎖當鎖且處於wait狀態的線程; - 某個鎖對象的notifyAll方法會把
所有
使用該鎖當鎖且處於wait狀態的線程都喚醒;
使用建議:
爲了防止某些線程無法被通知到,建議都使用notifyAll。
2 兩個比較經典的使用案例
感覺上學的時候好像就考過下面這兩個案例☺☺☺
2.1 案例1 — ABCABC。。。三個線程順序打印問題
2.1.1 題目
三個線程,線程A不停打印A、線程B不停的打印B、線程C不停的打印C,如何通過synchronized、wait、notifyAll(或notify)的組合,使三個線程不停地且順序地打印出ABCABC。。。
2.1.2 題目分析
其實我在《【併發編程】— Thread類中的join方法》這篇文章裏用join實現過類似的功能,有興趣的可以看一下。。。
如果使用synchronized、wait、notifyAll(或notify)的組合的話,這個問題可以歸結爲下圖所示的問題。即:
線程A走完 ,線程B走 —> 線程B走完,線程C走 —》 線程C走完,線程A走 。。。。
以線程A爲起點進行分析,可知:
-
(1)要想線程A走完,線程B接着走,那肯定是線程A釋放了線程B所需要的鎖,這裏設該鎖爲U,做進一步分析可知:
- 既然線程B需要線程A釋放的鎖U,那就意味着此時線程B中的鎖U肯定處於wait狀態;
- 同時要想線程A釋放了鎖U之後,線程B可以被喚醒,線程A還必須得進行鎖U的notify或notifyAll
-
(2)同理,要想線程B走完,線程C走,那肯定是線程C有一把處於wait狀態的鎖,這裏設爲V,需要線程B進行該鎖的notify或notifyAll 並釋放
-
(3)再同理,要想線程C走完,線程A接着走,那肯定是線程A有一把處於wait的鎖,這裏設爲W,需要線程C進行該鎖的notify或notifyAll 並釋放
用圖可以表示成下面的樣子:
分析到這裏我們可以再提煉一下:
- (1)每個線程都應該有兩把鎖
- (2)第一把鎖是
前面的
線程釋放後自己要搶到的鎖、第二把鎖是自己
要notify或notifyAll的鎖,對應到每個線程,就可以這樣描述- 線程A需要兩把鎖,一把爲線程C需要notify(或notifyAll)+ 釋放的鎖,可以認爲該鎖爲C鎖;另一把是自己需要notify(或notifyAll)+釋放的鎖,可以認爲該鎖爲A鎖
- 同理,線程B需要A線程notify(或notifyAll)+ 釋放的鎖A鎖,自己需要notify(或notifyAll)+釋放的B鎖
- 再同理,線程C需要B線程notify(或notifyAll)+ 釋放的鎖B鎖,自己需要notify(或notifyAll)+釋放的C鎖
分析到這裏後,可以將上圖改成下面的樣子,這樣理解起來,我感覺會更好一些:
分析到這裏就可以寫代碼了。
2.1.3 我的答案
- code
package com.nrsc.ch1.base.producer_consumer.ABCABC;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
@Slf4j
@AllArgsConstructor
public class ABCABC implements Runnable {
private String obj;
//前一個線程需要釋放,本線程需要wait的鎖
private Object prev;
//本線程需要釋放,下一個線程需要wait的鎖
private Object self;
@Override
public void run() {
int i = 3;
while (i > 0) {
//爲了在控制檯好看到效果,我這裏打印3輪
synchronized (prev) { //搶前面線程的鎖
synchronized (self) {// 搶到自己應該釋放的鎖
System.out.println(obj);
i--;
self.notifyAll(); //喚醒其他線程搶self
}//釋放自己應該釋放的鎖
try {
//走到這裏本線程已經釋放了自己應該釋放的鎖,接下來就需要讓自己需要等待的鎖進行等待就可以了
if (i > 0) { //我最開始沒加這個條件,但是測試發現程序沒停,其實分析一下就可以知道
//當前面i--使i=0了,其實該線程就已經完成3次打印了,就不需要再等前面的鎖了
//因此這裏加了該if判斷
prev.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Object lockA = new Object();
Object lockB = new Object();
Object lockC = new Object();
//線程A需要等待C線程釋放的鎖,同時需要釋放本線程該釋放的鎖A
new Thread(new ABCABC("A", lockC, lockA)).start();
Thread.sleep(1); //確保開始時A線程先執行
//線程B需要等待A線程釋放的鎖,同時需要釋放本線程該釋放的鎖B
new Thread(new ABCABC("B", lockA, lockB)).start();
Thread.sleep(1); //確保開始時B線程第2個執行
//線程C需要等待B線程釋放的鎖,同時需要釋放本線程該釋放的鎖C
new Thread(new ABCABC("C", lockB, lockC)).start();
}
}
- 測試結果:
2.2 生產者消費者問題
2.2.1 題目
如下圖所示:
- (1)有多個生產者,每個生產者都在不斷的搶麪包廠裏的機器生產麪包 —> 某個時間段只能有一個生產者進行生產
- (2)廠裏最多能存儲20箱,也就是說當已經有20箱了,各個生產者就不能生產了,需要等待消費者消費了,才能繼續生產
- (3)消費者也有多個,他們也會搶着去麪包廠買麪包,但也是某個時間段,只能有一個消費者搶到買麪包的資格
在以上條件的基礎上,寫一個多線程程序,保證在生產者不斷生產麪包的同時,消費者也在不斷的購買麪包。
(注意:
不能寫成生產者先生產了20箱,然後消費者再去消費20箱)
2.2.2 題目分析
其實我覺得這個很簡單,只需要想明白下面的兩點肯定就可以把這個代碼寫出來。
對於生產者
- (1)它們要不停地生產,直到麪包的箱數大於等於20時,生產者就等待 —> 等着消費者去消費
- (2)當面包的箱數小於20時,搶到生產權的生產者就生產,並通知消費者,我剛生產了一個,你們可以再繼續消費了
對於消費者
- (1)他們要不停地消費,知道麪包的箱數爲0時,它們就等待 —> 等着生產這去生產
- (2)當面包的箱數大於0時,搶到消費權的消費者就消費,並通知生產者,我剛消費了一個,你們可以再繼續生產了
2.2.3 我的答案
- 生產者和消費者
package com.nrsc.ch1.base.producer_consumer.multi;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BreadProducerAndConsumer2 {
/***麪包集合*/
private int i = 0;
/***
* 生產者 ,注意這裏鎖是當前對象,即this
*/
public synchronized void produceBread() {
//如果大於等於20箱,就等待 --- 如果這裏爲大於20的話,則20不會進入while,則會生產出21箱,所以這裏應爲>=
while (i >= 20) {
try {
this.wait();
} catch (InterruptedException e) {
log.error("生產者{},等待出錯", Thread.currentThread().getName(), e);
}
}
//如果不到20箱就繼續生產
i++; //生產一箱
log.warn("{}生產一箱麪包,現有面包{}個", Thread.currentThread().getName(), i);
//生產完,通知消費者進行消費
this.notifyAll();
}
/***
* 消費者
*/
public synchronized void consumeBread() {
//如果沒有了就等待
while (i <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
log.error("消費者{},等待出錯", Thread.currentThread().getName(), e);
}
}
//能走到這裏說明i>0,所以進行消費
i--; //消費一箱
log.info("{}消費一個麪包,現有面包{}個", Thread.currentThread().getName(), i);
//消費完,通知生產者進行生產
this.notifyAll();
}
}
- 測試類
package com.nrsc.ch1.base.producer_consumer.multi;
public class MultiTest {
public static void main(String[] args) throws InterruptedException {
BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2();
/***
* 不睡眠幾秒,效果不是很好,
* 因此我在
* 生產者線程裏睡了12秒 --- 因爲我覺得生產麪包的時間應該長 ☻☻☻
* 消費者線程裏睡了6秒 --- 因爲我覺得買麪包的時間應該快 ☻☻☻
*/
//生產者線程
for (int i = 0; i < 6; i++) {
new Thread(() -> {
//每個線程都不停的生產
while (true) {
try {
Thread.sleep(12);
} catch (InterruptedException e) {
e.printStackTrace();
}
pc.produceBread();
}
}, "生產者" + i).start();
}
//消費者線程
for (int i = 0; i < 6; i++) {
new Thread(() -> {
//每個線程都不停的消費
while (true) {
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
pc.consumeBread();
}
}, "消費者" + i).start();
}
}
}
- 測試效果如下: