文章目錄
- 一、前言
-
- 1. 什麼是線程間的通信
- 2. 提供一個簡單的業務場景
- 二、 wait及notify的使用
-
- 1. wait方法的API簡介
- 2. notify方法的API簡介
- 3. 新的業務需求
- 4. 線程的假死
- 5. notifyAll
- 三、補充
-
- 1. wait和notify方法的鎖狀态
- 2. wait和sleep的差別
一、前言
1. 什麼是線程間的通信
我想看到标題很多人想到的第一反應大概是疑問什麼是線程間的通信?其實這個概念很好了解,在我們的實際的業務開發中,很多場景都是多個線程之間配合進行工作的,就好比一條工廠的流水線的勞工,每個勞工之間都有分工,其中一部分勞工負責生産零件然後将零件傳遞給下一部分的勞工再進行加工,直到生産完完整的商品。線程之間在很多業務場景下也是如此,某條線程負責生産業務需求然後再分發給其他線程進項處理,無疑在多條線程配合的情況下,多線程之間需要進行有效的溝通才可以提高工作的效率,那麼線程之間溝通的方式其實就是線程間的通信。
2. 提供一個簡單的業務場景
俗話說包治百病,大多數女生對于男朋友送個包包都是不會拒絕的,尤其是當你送的包包是個限量的定制款的時候,你的女朋友并不會吝啬送給你一個香吻,那麼我們就以定制款的包包做個需求提供方和需求處理方的業務場景模型:
- 需求提供方Producer(買方):負責提出定制包包的需求。
需求處理方式Consumer(賣方):包包廠商,負責生産包包。
這裡注意: 由于此時賣方還是個小廠商,一次僅能處理一條定制請求,是以在他的官網每次僅開放一個包包的定制需求。商戶每日做多制作10個包包。
好了,我們先建立買方的Demo:
public class BagsProducer {
// 建立一個集合來裝定制請求
private List<String> list;
// 建立消費方法 msg為定制需求
public void productMsg(String msg) {
list.add(msg);
System.out.println("您需求為: " + msg + "已經開始定制,請耐心等待,接單時間"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
}
// 提供有參構造
public BagsProducer(List<String> list) {
super();
this.list = list;
}
}
然後在提供一個廠商的demo:
public class BagsConsumer {
// 建立一個集合來裝定制請求
private List<String> list;
// 每天開放定制數
private final static int MAX = 10;
// 包包編号
private int count = 1;
public void msgConsuption() {
try {
// 如果list大于0 辨別此時有定制需求
while (true) {
if (count > MAX) {
System.err.println("今日商鋪停止接單!");
return;
}
if (list.size() > 0) {
String string = list.get(0);
System.out.println("需求為: " + string + "的包包已經開始制作,開始制作時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
//計數
System.err.println("計數器狀态:"+ count++);
list.remove(0);
// 通過線程休眠 模拟包包制作過程
Thread.sleep(1_000);
System.out.println("需求為: " + string + "的包包已經制作完成,完成時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 提供有參構造
public BagsConsumer(List<String> list) {
super();
this.list = list;
}
}
最後我們提供一個啟動類:
public class ClassRunner {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
BagsProducer bagsProducer = new BagsProducer(list);
BagsConsumer bagsConsumer = new BagsConsumer(list);
// 啟動一号線程負責提供定制需求
new Thread(() -> {
for (int i = 0; i < 10; i++) {
bagsProducer.productMsg("貴就完事了");
}
},"消費者線程").start();
new Thread(() -> {
bagsConsumer.msgConsuption();
},"廠家線程").start();
}
}
運作main方法,運作結果如下(這裡由于程式存在BUG,需要在運作結束後手動退出線程!):
觀察運作結果,我們發現這中間有很多的問題,首先我們看這裡:
在這裡我們是通過一個for循環調用的bagsProducer.productMsg方法,由于這裡我們無法感覺到bagsConsumer.msgConsuption是否已經将上一條請求資訊處理結束,是以一次性的将所有請求資料都交給bagsProducer對象處理後失敗,造成商家無法接到後續訂單,是以我們對需求作如下優化:
- 使用者送出定制需求,首先判斷此時商家是否符合接單狀态,不符合則使用者需要等待商家可以接單後才可以再次下單
- 使用者如下單成功,此時使用者的預訂接口應該為暫時不可用狀态
- 商家接單後.處理定制需求,處理成功後将接單狀态修改為可以接單
在這裡我們發現通過之前的技術棧來實作暫停接單的業務無疑是有點複雜的,好在JDK已經為我們提供好了配套使用的API來完成生産/消費模型
二、 wait及notify的使用
1. wait方法的API簡介
首先我們可以打開JDK提供的API文檔,先檢視下wait方法的描述:
其中标紅的地方我們需要着重的講一下
- 根據API描述我們可以看出當我們調用wait方法後,目前線程會進入等待狀态(WAITING),一直到其他方法調用notify或者notifyAll,線程才會重新變為就緒态(RUNNABLE),也就是說明如果直接調用wait方法并不設定等待時間的話,該線程無法主動地喚醒自己,需要由其他線程調用方法進行喚醒。
- 這裡圖中的第二點和第三點其實講的就是同一個事情,與sleep方法不同隻能被Thread類調用不同,wait方法可以被任意Object對象調用,但是這裡我們需要注意的是,調用wait方法的對象需要持有monitor對象,可以簡單地了解為鎖 (文檔中的螢幕是monitor的中文翻譯,monitor與鎖相似但是存在部分差異)。
- 當我們調用wait方法的時候,此時目前線程會釋放所持有的鎖
針對以上三點我們先做一個實驗
- 為了證明上述第二點,我們先直接的調用wait方法,觀察現象:
// 修改下消費者的productMsg方法 添加wait
public class BagsProducer {
// 建立一個集合來裝定制請求
private List<String> list;
// 建立消費方法 msg為定制需求
public void productMsg(String msg) {
try {
System.out.println("請求接單");
if (list.size() > 0) {
list.wait();
return;
}
list.add(msg);
System.out.println("您需求為: " + msg + "已經開始定制,請耐心等待,接單時間"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 提供有參構造
public BagsProducer(List<String> list) {
super();
this.list = list;
}
}
啟動ClassRunner,觀察結果:
這裡看到消費者線程抛出了IllegalMonitorStateException異常,API文檔中對該異常的描述為:
這裡我們證明了調用wait方法需要持有鎖才可以,解決這個問題我們隻需要通過synchronized同步代碼快為目前線程提供一個鎖:
public class BagsProducer {
// 建立一個集合來裝定制請求
private List<String> list;
// 建立消費方法 msg為定制需求
public void productMsg(String msg) {
try {
System.out.println("請求接單");
if (list.size() > 0) {
synchronized (list) {
list.wait();
//這裡删除了return
}
}
list.add(msg);
System.out.println("您需求為: " + msg + "已經開始定制,請耐心等待,接單時間"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 提供有參構造
public BagsProducer(List<String> list) {
super();
this.list = list;
}
}
這裡我們通過CMD指令打開DOM視窗,然後輸入jps查找線程号,然後通過jstack + 線程号 的方式檢視線程狀态:
此時消費者線程已經進入休眠,完成了我們預期的對于消費者線程的修改,接下來我們就要繼續修改商家線程來配合wait方法進行業務優化。
2. notify方法的API簡介
我們還是按照之前的習慣,先檢視下API文檔對于notify方法的描述:
對于紅線處的了解我們可以概括為如下幾點:
- notify方法主要用來喚醒同一個monitor對象下的wait狀态的線程,這裡需要注意的是,調用notify方法隻會喚醒目前monitor下的一個線程而不是全部,如果有多個所屬目前monitor對象的線程處于wait狀态,隻會随機喚醒其中一條而不是全部。
- 被喚醒的線程狀态為就緒态(runnable),此時被喚醒的線程需要與其他線程重新争奪鎖
- notify可以被任意Object對象調用,調用的前提是需要持有monitor對象(螢幕)
- 調用notify對象不會立即釋放鎖,這點需要注意
這裡我們開始進行BagsConsumer對象的改造:
public class BagsConsumer {
// 建立一個集合來裝定制請求
private List<String> list;
// 每天開放定制數
private final static int MAX = 10;
// 包包編号
private int count = 1;
public void msgConsuption() {
try {
// 如果list大于0 辨別此時有定制需求
while (true) {
if (count > MAX) {
System.err.println("今日商鋪停止接單!");
return;
}
if (list.size() > 0) {
String string = list.get(0);
System.out.println("需求為: " + string + "的包包已經開始制作,開始制作時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
//計數
System.err.println("計數器狀态:"+ count++);
list.remove(0);
// 通過線程休眠 模拟包包制作過程
Thread.sleep(1_000);
System.out.println("需求為: " + string + "的包包已經制作完成,完成時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
synchronized (list) {
list.notify();
}
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 提供有參構造
public BagsConsumer(List<String> list) {
super();
this.list = list;
}
}
運作結果:
這裡看到程式已經按照我們的預期實作了業務處理。但是顯然我們不會滿足于這麼簡單的也無需求,畢竟商家也會做強做大的是不是,那麼我們就要提出新的業務模型來引出我們下面的知識點。
3. 新的業務需求
過了段時間,越來越多的商家發現商機,開始制作定制包包,而每個包包廠家由于能夠接到的單子有限,為了節約成本,商家在沒有單子的時候會給勞工放假,直到系統派單為止,我們來總結下需求:
- 商戶端:現在包包的定制商分為LV,Hermes(愛馬仕),Chanel(香奈兒)三家,每家每日的接單上限依舊為10個,為了提高定制品質,同一時間同一商家隻能接一個訂單,為了節約人力成本,員工需要等到使用者下單後才需要上班。
- 使用者端:使用者現在下單需要做兩個判斷,第一點使用者依舊每次隻能下一個單,需要等待上一個訂單完成使用者倒賣後才能有錢下下個訂單,使用者需要等待商家空閑後才可下單,每次下單後需要通知一個商家開工。
這裡我們先對新的需求進行實作,觀察是否有新的問題發生
使用者端:
public class Client {
// 建立一個集合來裝定制請求 此時集合允許最多存放三個請求
private List<String> list;
// 建立消費方法 msg為定制需求
public void productMsg(String msg) {
try {
// 這裡要判斷list的size是否大于1 當size大于1的時候代表此時三個商家都已經接單處于制作中,按照需求此時使用者不允許下單
if (list.size() > 0) {
synchronized (list) {
list.wait();
}
}
// 使用者下單
list.add(msg);
System.out.println();
System.out.println("需求為" + msg + "的包包已下單,訂單建立時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
// 下單後使用者需要通知一個商家上班
synchronized (list) {
list.notify();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public Client(List<String> list) {
super();
this.list = list;
}
}
商家端
public class Business {
// 建立一個集合來裝定制請求
private List<String> list;
// 建立每日最大接單
private final static int MAX = 10;
// 建立計數器
private int count = 1;
public void make() {
try {
while (true) {
String demand;
synchronized (list) {
if (count > 10) {
System.err.println(Thread.currentThread().getName() + "商戶停止接單");
System.out.println();
// 商戶下班
return;
}
// 沒有需求的時候 商戶休息
if (list.size() == 0) {
list.wait();
continue;
}
count++;
demand = list.remove(0);
System.out.println(Thread.currentThread().getName() + "已接單需求為" + demand + "的包包,接單時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
}
// 通過休眠 模拟制作過程
Thread.sleep(1_000);
System.out.println("需求為" + demand + "的包包已制作完成,完成時間為:"
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
// 此時通知客戶可以繼續下單
synchronized (list) {
list.notify();
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 有參構造
public Business(List<String> list) {
super();
this.list = list;
}
}
啟動類:
public class ClassRunner {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
new Thread(() -> {
Client client = new Client(list);
for (int i = 1; i < 31; i++) {
client.productMsg("聖誕節限定款編号為" + i);
}
}, "買家線程").start();
try {
// 通過休眠 模拟下單延時時間
Thread.sleep(1_000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//建立三個商家
Stream.of("LV", "Hermes","Chanel").forEach(s -> {
new Thread(() -> {
Business business = new Business(list);
business.make();
}, s).start();
});
}
}
啟動觀察結果:
這裡可以看到,使用者在下了兩次單後就停止了下單,明顯不符合我們的業務邏輯,這是為什麼呢?
4. 線程的假死
我們通過jstack的方式觀察下此時線程的狀态:
這裡我們發現我們的買家線程和三個賣家線程全部進入到了wait狀态,造成這個現象的主要原因是因為notify方法的特性為随機喚醒一條wait線程,于是就出現了下面的業務場景:
- 使用者提供了一個定制需求後調用notify方法喚醒一個商家制作包包。
- 使用者線程重新進入判斷,由于此時有未完成定制,使用者線程進入wait狀态
- 三個商家之一接到notify指令喚醒變為runnable狀态,開始執行使用者定制需求,處理結束後,使用者調用notify方法随機喚醒一個等待線程。
- 此時另外一個wait商家接到喚醒指令,裝換為runnable狀态,但是此時并沒有使用者提供新的需求,繼而轉為wait狀态。
- 至此四個線程全部進入wait狀态,出現假死現象
5. notifyAll
解決上述的假死狀态很簡單,将商家端(Business對象)的notify方法更換為notifyAll即可。
notify和notifyAll的差別:
- notify方法随機喚醒一條monitor對象下的wait線程
- notifyAll方法喚醒全部屬于此monitor對象下的wait線程
更換後運作結果如下:
至此應用多線程通信的一對多的消費模型簡單實作就完成了
三、補充
1. wait和notify方法的鎖狀态
- 當線程調用wait方法後會立即釋放鎖
- 當線程調用notify方法後會等目前線程全部業務代碼執行完畢後才會釋放鎖
2. wait和sleep的差別
1.所屬調用方不同
sleep隻能由Thread來調用,而wait可以被所有Object對象調用。
2.鎖狀态不同
wait方法會立即釋放鎖,而sleep會繼續持有鎖知道所有業務邏輯處理結束。
3.是否需要monitor
wait方法需要目前線程持有monitor,sleep則不需要。
至此,今天要講的wait和notify的應用就全部結束了,如果代碼中存在錯誤或不清楚的地方歡迎指正,如果覺得看了這篇文章有收獲的同學,希望可以點個贊加個關注~
祝好!