轉載請備註地址:https://juejin.im/post/5ab755fc6fb9a028c22aba1f
系列文章傳送門:
Java多執行緒學習(二)synchronized關鍵字(1)
Java多執行緒學習(二)synchronized關鍵字(2)
Java多執行緒學習(四)等待/通知(wait/notify)機制
系列文章將被優先更新於微信公眾號“Java面試通關手冊”,歡迎廣大Java程式設計師和愛好技術的人員關注。
本節思維導圖:
思維導圖原始檔+思維導圖軟體關注微信公眾號:“Java面試通關手冊” 回覆關鍵字:“Java多執行緒” 免費領取。
一 等待/通知機制介紹
1.1 不使用等待/通知機制
當兩個執行緒之間存在生產和消費者關係,也就是說第一個執行緒(生產者)做相應的操作然後第二個執行緒(消費者)感知到了變化又進行相應的操作。比如像下面的whie語句一樣,假設這個value值就是第一個執行緒操作的結果,doSomething()是第二個執行緒要做的事,當滿足條件value=desire後才執行doSomething()。
但是這裡有個問題就是:第二個語句不停過通過輪詢機制來檢測判斷條件是否成立。如果輪詢時間的間隔太小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到自己想要的資料。所以這裡就需要我們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾。
while(value=desire){
doSomething();
}
複製程式碼
1.2 什麼是等待/通知機制?
通俗來講:
等待/通知機制在我們生活中比比皆是,一個形象的例子就是廚師和服務員之間就存在等待/通知機制。
- 廚師做完一道菜的時間是不確定的,所以菜到服務員手中的時間是不確定的;
- 服務員就需要去“等待(wait)”;
- 廚師把菜做完之後,按一下鈴,這裡的按鈴就是“通知(nofity)”;
- 服務員聽到鈴聲之後就知道菜做好了,他可以去端菜了。
用專業術語講:
等待/通知機制,是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()/notifyAll()方法,執行緒A收到通知後退出等待佇列,進入可執行狀態,進而執行後續操作。上訴兩個執行緒通過物件O來完成互動,而物件上的wait()方法和notify()/notifyAll()方法的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。
1.3 等待/通知機制的相關方法
方法名稱 | 描述 |
---|---|
notify() | 隨機喚醒等待佇列中等待同一共享資源的 “一個執行緒”,並使該執行緒退出等待佇列,進入可執行狀態,也就是notify()方法僅通知“一個執行緒” |
notifyAll() | 使所有正在等待佇列中等待同一共享資源的 “全部執行緒” 退出等待佇列,進入可執行狀態。此時,優先順序最高的那個執行緒最先執行,但也有可能是隨機執行,這取決於JVM虛擬機器的實現 |
wait() | 使呼叫該方法的執行緒釋放共享資源鎖,然後從執行狀態退出,進入等待佇列,直到被再次喚醒 |
wait(long) | 超時等待一段時間,這裡的引數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回 |
wait(long,int) | 對於超時時間更細力度的控制,可以達到納秒 |
二 等待/通知機制的實現
2.1 我的第一個等待/通知機制程式
MyList.java
public class MyList {
private static List<String> list = new ArrayList<String>();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
複製程式碼
ThreadA.java
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait begin "
+ System.currentTimeMillis());
lock.wait();
System.out.println("wait end "
+ System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
ThreadB.java
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已發出通知!");
}
System.out.println("新增了" + (i + 1) + "個元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(50);
ThreadB b = new ThreadB(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
執行結果:
從執行結果:"wait end 1521967322359"最後輸出可以看出,notify()執行後並不會立即釋放鎖。下面我們會補充介紹這個知識點。synchronized關鍵字可以將任何一個Object物件作為同步物件來看待,而Java為每個Object都實現了等待/通知(wait/notify)機制的相關方法,它們必須用在synchronized關鍵字同步的Object的臨界區內。通過呼叫wait()方法可以使處於臨界區內的執行緒進入等待狀態,同時釋放被同步物件的鎖。而notify()方法可以喚醒一個因呼叫wait操作而處於阻塞狀態中的執行緒,使其進入就緒狀態。被重新喚醒的執行緒會檢視重新獲得臨界區的控制權也就是鎖,並繼續執行wait方法之後的程式碼。如果發出notify操作時沒有處於阻塞狀態中的執行緒,那麼該命令會被忽略。
如果我們這裡不通過等待/通知(wait/notify)機制實現,而是使用如下的while迴圈實現的話,我們上面也講過會有很大的弊端。
while(MyList.size() == 5){
doSomething();
}
複製程式碼
2.2執行緒的基本狀態
上面幾章的學習中我們已經掌握了與執行緒有關的大部分API,這些API可以改變執行緒物件的狀態。如下圖所示:
-
新建(new):新建立了一個執行緒物件。
-
可執行(runnable):執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲 取cpu的使用權。
-
執行(running):可執行狀態(runnable)的執行緒獲得了cpu時間片(timeslice),執行程式程式碼。
-
阻塞(block):阻塞狀態是指執行緒因為某種原因放棄了cpu使用權,也即讓出了cpu timeslice,暫時停止執行。直到執行緒進入可執行(runnable)狀態,才有 機會再次獲得cpu timeslice轉到執行(running)狀態。阻塞的情況分三種:
(一). 等待阻塞:執行(running)的執行緒執行o.wait()方法,JVM會把該執行緒放 入等待佇列(waitting queue)中。
(二). 同步阻塞:執行(running)的執行緒在獲取物件的同步鎖時,若該同步鎖 被別的執行緒佔用,則JVM會把該執行緒放入鎖池(lock pool)中。
(三). 其他阻塞: 執行(running)的執行緒執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入可執行(runnable)狀態。
-
死亡(dead):執行緒run()、main()方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。
備註: 可以用早起坐地鐵來比喻這個過程:
還沒起床:sleeping
起床收拾好了,隨時可以坐地鐵出發:Runnable
等地鐵來:Waiting
地鐵來了,但要排隊上地鐵:I/O阻塞
上了地鐵,發現暫時沒座位:synchronized阻塞
地鐵上找到座位:Running
到達目的地:Dead
2.3 notify()鎖不釋放
當方法wait()被執行後,鎖自動被釋放,但執行玩notify()方法後,鎖不會自動釋放。必須執行完otify()方法所在的synchronized程式碼塊後才釋放。
下面我們通過程式碼驗證一下:
(完整程式碼:github.com/Snailclimb/…)
帶wait方法的synchronized程式碼塊
synchronized (lock) {
System.out.println("begin wait() ThreadName="
+ Thread.currentThread().getName());
lock.wait();
System.out.println(" end wait() ThreadName="
+ Thread.currentThread().getName());
}
複製程式碼
帶notify方法的synchronized程式碼塊
synchronized (lock) {
System.out.println("begin notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
lock.notify();
Thread.sleep(5000);
System.out.println(" end notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
}
複製程式碼
如果有三個同一個物件例項的執行緒a,b,c,a執行緒執行帶wait方法的synchronized程式碼塊然後bb執行緒執行帶notify方法的synchronized程式碼塊緊接著c執行帶notify方法的synchronized程式碼塊。
執行效果如下:
這也驗證了我們剛開始的結論:必須執行完notify()方法所在的synchronized程式碼塊後才釋放。2.4 當interrupt方法遇到wait方法
當執行緒呈wait狀態時,對執行緒物件呼叫interrupt方法會出現InterrupedException異常。
Service.java
public class Service {
public void testMethod(Object lock) {
try {
synchronized (lock) {
System.out.println("begin wait()");
lock.wait();
System.out.println(" end wait()");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("出現異常了,因為呈wait狀態的執行緒被interrupt了!");
}
}
}
複製程式碼
ThreadA.java
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
Service service = new Service();
service.testMethod(lock);
}
}
複製程式碼
Test.java
public class Test {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(5000);
a.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
執行結果:
參考:
《Java多執行緒程式設計核心技術》
《Java併發程式設計的藝術》