多執行緒學習-day-04等待(wait)和通知(notify/notifyAll)
執行緒基礎、執行緒之間的共享和協作
(目前會將一些概念簡單描述,一些重點的點會詳細描述)
學習目標:等待和通知
一、應用場景:
一個執行緒修改了一個值,另一個執行緒感受到了值的變化,進行相應的操作。前一個執行緒類比於一個生產者,後一個執行緒是消費者。如何讓消費者感受到生產者的一個值的變化呢?
解決方案一:
輪詢:每隔一分鐘就去輪詢一次,總有一個時間點能夠獲取到生產者的變換。比如煲湯,每個一分鐘就去看一下是否煲好了。結果:這樣會很累,很佔用資源。
輪詢的缺點:很難確保一個及時性,每隔一段時間就要去操作一次,資源開銷很大,做很多無用功。
解決方案二:
等待和通知機制方式:當一個執行緒呼叫了wait()方法,會進入一個等待狀態,而另外一個執行緒對值進行操作後,呼叫notify()或者notifyAll()方法後,通知第一個執行緒去操作某件事情。注意:wait()、notify()/notifyAll()是物件上的方法。
wait()等待方會怎麼做?
1、獲取物件的鎖;一定是在迴圈裡面去操作;
2、迴圈裡判斷是否滿足條件,不滿足條件呼叫wait()方法,一直等待;
3、滿足條件,執行業務邏輯;
notify()、notifyAll()會怎麼做?
1、依然要獲取物件的鎖;
2、改變相關條件;
3、通知所有等待在物件的執行緒
以上介紹了wait、notify/notifyAll的標準正規化。
三、notify()和notifyAll()區別:
應該儘量應用notifyAll(),使用notify()的話,jvm會執行已經加入等待執行緒棧裡面的第一個執行緒,給我們一種感觀就是隨機的選擇了一種執行緒,如果該執行緒達到條件就正好執行那一條,其實這是一個誤區,而是jvm會選擇線上程棧裡面的第一個執行緒。因此如果用notify()的話,可能會造成訊號丟失的情況。
舉例應用:比如一個快遞,發貨地址是長沙,收貨地址是深圳,初始發貨公里(km)為0。則改變公里數(km)以及改變收貨地址,來操作等待/通知的情景
先定義一個Express類
public class Express { // 定義一個發貨地,這裡定為長沙 public static final String CITY = "ChangSha"; // 定義一個千米數,表示貨已經走了多少千米了 private int km; // 定義一個發貨城市,表示貨從哪個城市出發 private String site; // 定義無參構造方法 public Express() { } // 定義有參構造方法 public Express(int km, String site) { this.km = km; this.site = site; } // 定義改變km的方法 public synchronized void changeKm() { // 定義已經走了101千米了 km = 101; // 千米數改變之後,就傳送通知 notifyAll(); } // 定義改變城市的方法 public synchronized void changeSite() { // 定義深圳收貨 site = "ShenZhen"; // 城市改變後,就傳送通知 notifyAll(); } // 定義千米等待的方法,等待km改變之後,就取消等待,輸出結果 public synchronized void waitKm() { // 當km小於100時,還沒有超過100km,則繼續等待超過100km後的通知 while (this.km <= 100) { try { wait(); System.out.println("當前執行緒:" + Thread.currentThread().getId() + " 還沒有運送超過100km,還在等通知!"); } catch (InterruptedException e) { e.printStackTrace(); } } // 收到通知後,等待結束,輸出結果 System.out.println("當前km = " + this.km + " 已經運送超過100km了,進行資料庫儲存操作!"); } // 定義城市等待的方法,等待城市改變之後,就取消等待,輸出結果 public synchronized void waitSite() { // 當site還沒有到長沙,則繼續等待到長沙之後的通知 while (CITY.equals(this.site)) { try { wait(); System.out.println("當前執行緒:" + Thread.currentThread().getId() + " 還沒有到達長沙,還在等通知!"); } catch (InterruptedException e) { e.printStackTrace(); } } // 收到通知後,表示到達長沙,等待結束,輸出結果 System.out.println("當前site = " + this.site + "已經到達深圳,要告訴使用者下來拿快遞!"); } }
定義測試類TestWN(Test Wait Notifyall縮寫)
public class TestWN {
// 例項化Express物件,並呼叫物件鎖
private static Express express = new Express(0, Express.CITY);
// 定義方法,檢查km變化情況,沒有達到條件一直等待
public static class ThreadKm extends Thread {
@Override
public void run() {
// 因為Express裡面的方法都是物件鎖,所以直接呼叫
express.waitKm();
}
}
// 定義方法,檢查site變化情況,沒有達到條件一直等待
public static class ThreadSite extends Thread {
@Override
public void run() {
// 因為Express裡面的方法都是物件鎖,所以直接呼叫
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
// 設定site變化
for (int i = 0; i < 3; i++) {
new ThreadSite().start();
}
// 設定km變化
for (int i = 0; i < 3; i++) {
new ThreadKm().start();
}
// 休眠3秒,然後改變里程數
Thread.sleep(1000);
express.changeKm();
express.changeSite();
}
}
控制檯輸出結果:
當前執行緒:15 還沒有運送超過100km,還在等通知!
當前km = 101 已經運送超過100km了,進行資料庫儲存操作!
當前執行緒:14 還沒有運送超過100km,還在等通知!
當前km = 101 已經運送超過100km了,進行資料庫儲存操作!
當前執行緒:13 還沒有運送超過100km,還在等通知!
當前km = 101 已經運送超過100km了,進行資料庫儲存操作!
當前執行緒:12 還沒有到達長沙,還在等通知!
當前site = ShenZhen已經到達深圳,要告訴使用者下來拿快遞!
當前執行緒:11 還沒有到達長沙,還在等通知!
當前site = ShenZhen已經到達深圳,要告訴使用者下來拿快遞!
當前執行緒:10 還沒有到達長沙,還在等通知!
當前site = ShenZhen已經到達深圳,要告訴使用者下來拿快遞!
以上介紹的就是等待/通知標準正規化的程式碼演示。
join()方法
執行緒A,執行了執行緒B的join方法,執行緒A必須要等待B執行完成了以後,執行緒A才能繼續自己的工作
呼叫yield() 、sleep()、wait()、notify()等方法對鎖有何影響?
執行緒在執行yield()以後,持有的鎖是不釋放的
sleep()方法被呼叫以後,持有的鎖是不釋放的
調動方法之前,必須要持有鎖。呼叫了wait()方法以後,鎖就會被釋放,當wait方法返回的時候,執行緒會重新持有鎖
調動方法之前,必須要持有鎖,呼叫notify()方法本身不會釋放鎖的
來自享學IT教育課後總結。