Java多線程學習之wait、notify/notifyAll 詳解

https://www.cnblogs.com/moongeek/p/7631447.html

https://blog.51cto.com/12304309/2138845

1、wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。

2、wait()使當前線程阻塞,前提是 必須先獲得鎖,一般配合synchronized 關鍵字使用,即,一般在synchronized 同步代碼塊裏使用 wait()、notify/notifyAll() 方法。

3、 由於 wait()、notify/notifyAll() 在synchronized 代碼塊執行,說明當前線程一定是獲取了鎖的。

當線程執行wait()方法時候,會釋放當前的鎖,然後讓出CPU,進入等待狀態。

只有當 notify/notifyAll() 被執行時候,纔會喚醒一個或多個正處於等待狀態的線程,然後繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。

也就是說,notify/notifyAll() 的執行只是喚醒沉睡的線程,而不會立即釋放鎖,鎖的釋放要看代碼塊的具體執行情況。所以在編程中,儘量在使用了notify/notifyAll() 後立即退出臨界區,以喚醒其他線程 

4、wait() 需要被try catch包圍,中斷也可以使wait等待的線程喚醒。

5、notify 和wait 的順序不能錯,如果A線程先執行notify方法,B線程在執行wait方法,那麼B線程是無法被喚醒的。

6、notify 和 notifyAll的區別

notify方法只喚醒一個等待(對象的)線程並使該線程開始執行。所以如果有多個線程等待一個對象,這個方法只會喚醒其中一個線程,選擇哪個線程取決於操作系統對多線程管理的實現。notifyAll 會喚醒所有等待(對象的)線程,儘管哪一個線程將會第一個處理取決於操作系統的實現。如果當前情況下有多個線程需要被喚醒,推薦使用notifyAll 方法。比如在生產者-消費者裏面的使用,每次都需要喚醒所有的消費者或是生產者,以判斷程序是否可以繼續往下執行。

7、在多線程中要測試某個條件的變化,使用if 還是while?

  要注意,notify喚醒沉睡的線程後,線程會接着上次的執行繼續往下執行。所以在進行條件判斷時候,可以先把 wait 語句忽略不計來進行考慮,顯然,要確保程序一定要執行,並且要保證程序直到滿足一定的條件再執行,要使用while來執行,以確保條件滿足和一定執行。如下代碼:

複製代碼

 1 public class K {
 2     //狀態鎖
 3     private Object lock;
 4     //條件變量
 5     private int now,need;
 6     public void produce(int num){
 7         //同步
 8         synchronized (lock){
 9            //當前有的不滿足需要,進行等待
10             while(now < need){
11                 try {
12                     //等待阻塞
13                     wait();
14                 } catch (InterruptedException e) {
15                     e.printStackTrace();
16                 }
17                 System.out.println("我被喚醒了!");
18             }
19            // 做其他的事情
20         }
21     }
22 }
23             

複製代碼

顯然,只有當前值滿足需要值的時候,線程纔可以往下執行,所以,必須使用while 循環阻塞。注意,wait() 當被喚醒時候,只是讓while循環繼續往下走.如果此處用if的話,意味着if繼續往下走,會跳出if語句塊。但是,notifyAll 只是負責喚醒線程,並不保證條件云云,所以需要手動來保證程序的邏輯。

8、實現生產者和消費者問題

  什麼是生產者-消費者問題呢?

  如上圖,假設有一個公共的容量有限的池子,有兩種人,一種是生產者,另一種是消費者。需要滿足如下條件:

    1、生產者產生資源往池子裏添加,前提是池子沒有滿,如果池子滿了,則生產者暫停生產,直到自己的生成能放下池子。

    2、消費者消耗池子裏的資源,前提是池子的資源不爲空,否則消費者暫停消耗,進入等待直到池子裏有資源數滿足自己的需求。

  - 倉庫類

複製代碼

 1 import java.util.LinkedList;
 2 
 3 /**
 4  *  生產者和消費者的問題
 5  *  wait、notify/notifyAll() 實現
 6  */
 7 public class Storage1 implements AbstractStorage {
 8     //倉庫最大容量
 9     private final int MAX_SIZE = 100;
10     //倉庫存儲的載體
11     private LinkedList list = new LinkedList();
12 
13     //生產產品
14     public void produce(int num){
15         //同步
16         synchronized (list){
17             //倉庫剩餘的容量不足以存放即將要生產的數量,暫停生產
18             while(list.size()+num > MAX_SIZE){
19                 System.out.println("【要生產的產品數量】:" + num + "\t【庫存量】:"
20                         + list.size() + "\t暫時不能執行生產任務!");
21 
22                 try {
23                     //條件不滿足,生產阻塞
24                     list.wait();
25                 } catch (InterruptedException e) {
26                     e.printStackTrace();
27                 }
28             }
29 
30             for(int i=0;i<num;i++){
31                 list.add(new Object());
32             }
33 
34             System.out.println("【已經生產產品數】:" + num + "\t【現倉儲量爲】:" + list.size());
35 
36             list.notifyAll();
37         }
38     }
39 
40     //消費產品
41     public void consume(int num){
42         synchronized (list){
43 
44             //不滿足消費條件
45             while(num > list.size()){
46                 System.out.println("【要消費的產品數量】:" + num + "\t【庫存量】:"
47                         + list.size() + "\t暫時不能執行生產任務!");
48 
49                 try {
50                     list.wait();
51                 } catch (InterruptedException e) {
52                     e.printStackTrace();
53                 }
54             }
55 
56             //消費條件滿足,開始消費
57             for(int i=0;i<num;i++){
58                 list.remove();
59             }
60 
61             System.out.println("【已經消費產品數】:" + num + "\t【現倉儲量爲】:" + list.size());
62 
63             list.notifyAll();
64         }
65     }
66 }

複製代碼

  - 抽象倉庫類

複製代碼

1 public interface AbstractStorage {
2     void consume(int num);
3     void produce(int num);
4 }

複製代碼

  - 生產者

複製代碼

 1 public class Producer extends Thread{
 2     //每次生產的數量
 3     private int num ;
 4 
 5     //所屬的倉庫
 6     public AbstractStorage abstractStorage;
 7 
 8     public Producer(AbstractStorage abstractStorage){
 9         this.abstractStorage = abstractStorage;
10     }
11 
12     public void setNum(int num){
13         this.num = num;
14     }
15 
16     // 線程run函數
17     @Override
18     public void run()
19     {
20         produce(num);
21     }
22 
23     // 調用倉庫Storage的生產函數
24     public void produce(int num)
25     {
26         abstractStorage.produce(num);
27     }
28 }

複製代碼

  - 消費者

複製代碼

 1 public class Consumer extends Thread{
 2     // 每次消費的產品數量
 3     private int num;
 4 
 5     // 所在放置的倉庫
 6     private AbstractStorage abstractStorage1;
 7 
 8     // 構造函數,設置倉庫
 9     public Consumer(AbstractStorage abstractStorage1)
10     {
11         this.abstractStorage1 = abstractStorage1;
12     }
13 
14     // 線程run函數
15     public void run()
16     {
17         consume(num);
18     }
19 
20     // 調用倉庫Storage的生產函數
21     public void consume(int num)
22     {
23         abstractStorage1.consume(num);
24     }
25 
26     public void setNum(int num){
27         this.num = num;
28     }
29 }

複製代碼

  - 測試

複製代碼

 1 public class Test{
 2     public static void main(String[] args) {
 3         // 倉庫對象
 4         AbstractStorage abstractStorage = new Storage1();
 5 
 6         // 生產者對象
 7         Producer p1 = new Producer(abstractStorage);
 8         Producer p2 = new Producer(abstractStorage);
 9         Producer p3 = new Producer(abstractStorage);
10         Producer p4 = new Producer(abstractStorage);
11         Producer p5 = new Producer(abstractStorage);
12         Producer p6 = new Producer(abstractStorage);
13         Producer p7 = new Producer(abstractStorage);
14 
15         // 消費者對象
16         Consumer c1 = new Consumer(abstractStorage);
17         Consumer c2 = new Consumer(abstractStorage);
18         Consumer c3 = new Consumer(abstractStorage);
19 
20         // 設置生產者產品生產數量
21         p1.setNum(10);
22         p2.setNum(10);
23         p3.setNum(10);
24         p4.setNum(10);
25         p5.setNum(10);
26         p6.setNum(10);
27         p7.setNum(80);
28 
29         // 設置消費者產品消費數量
30         c1.setNum(50);
31         c2.setNum(20);
32         c3.setNum(30);
33 
34         // 線程開始執行
35         c1.start();
36         c2.start();
37         c3.start();
38 
39         p1.start();
40         p2.start();
41         p3.start();
42         p4.start();
43         p5.start();
44         p6.start();
45         p7.start();
46     }
47 }

複製代碼

  - 輸出

複製代碼

【要消費的產品數量】:50    【庫存量】:0    暫時不能執行生產任務!
【要消費的產品數量】:20    【庫存量】:0    暫時不能執行生產任務!
【要消費的產品數量】:30    【庫存量】:0    暫時不能執行生產任務!
【已經生產產品數】:10    【現倉儲量爲】:10
【要消費的產品數量】:30    【庫存量】:10    暫時不能執行生產任務!
【要消費的產品數量】:20    【庫存量】:10    暫時不能執行生產任務!
【要消費的產品數量】:50    【庫存量】:10    暫時不能執行生產任務!
【已經生產產品數】:10    【現倉儲量爲】:20
【已經生產產品數】:10    【現倉儲量爲】:30
【要消費的產品數量】:50    【庫存量】:30    暫時不能執行生產任務!
【已經消費產品數】:20    【現倉儲量爲】:10
【要消費的產品數量】:30    【庫存量】:10    暫時不能執行生產任務!
【已經生產產品數】:10    【現倉儲量爲】:20
【要消費的產品數量】:50    【庫存量】:20    暫時不能執行生產任務!
【要消費的產品數量】:30    【庫存量】:20    暫時不能執行生產任務!
【已經生產產品數】:10    【現倉儲量爲】:30
【已經消費產品數】:30    【現倉儲量爲】:0
【要消費的產品數量】:50    【庫存量】:0    暫時不能執行生產任務!
【已經生產產品數】:10    【現倉儲量爲】:10
【要消費的產品數量】:50    【庫存量】:10    暫時不能執行生產任務!
【已經生產產品數】:80    【現倉儲量爲】:90
【已經消費產品數】:50    【現倉儲量爲】:40

複製代碼

 

我自己總結的Java學習的系統知識點以及面試問題,目前已經開源,會一直完善下去,歡迎建議和指導歡迎Star: https://github.com/Snailclimb/Java-Guide

本節思維導圖:

本節思維導圖

一 等待/通知機制介紹

1.1 不使用等待/通知機制

當兩個線程之間存在生產和消費者關係,也就是說第一個線程(生產者)做相應的操作然後第二個線程(消費者)感知到了變化又進行相應的操作。比如像下面的whie語句一樣,假設這個value值就是第一個線程操作的結果,doSomething()是第二個線程要做的事,當滿足條件value=desire後才執行doSomething()。

但是這裏有個問題就是:第二個語句不停過通過輪詢機制來檢測判斷條件是否成立。如果輪詢時間的間隔太小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到自己想要的數據。所以這裏就需要我們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾。

    while(value=desire){
        doSomething();
    }

1.2 什麼是等待/通知機制?

通俗來講:

等待/通知機制在我們生活中比比皆是,一個形象的例子就是廚師和服務員之間就存在等待/通知機制。

  1. 廚師做完一道菜的時間是不確定的,所以菜到服務員手中的時間是不確定的;
  2. 服務員就需要去“等待(wait)”;
  3. 廚師把菜做完之後,按一下鈴,這裏的按鈴就是“通知(nofity)”;
  4. 服務員聽到鈴聲之後就知道菜做好了,他可以去端菜了。

用專業術語講:

等待/通知機制,是指一個線程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"最後輸出可以看出,<font color="red">notify()執行後並不會立即釋放鎖。</font>下面我們會補充介紹這個知識點。

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可以改變線程對象的狀態。如下圖所示:

線程的基本狀態切換圖

  1. 新建(new):新創建了一個線程對象。
  2. 可運行(runnable):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲 取cpu的使用權。
  3. 運行(running):可運行狀態(runnable)的線程獲得了cpu時間片(timeslice),執行程序代碼。
  4. 阻塞(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)狀態。
  1. 死亡(dead):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。

備註:

可以用早起坐地鐵來比喻這個過程:

還沒起牀:sleeping

起牀收拾好了,隨時可以坐地鐵出發:Runnable

等地鐵來:Waiting

地鐵來了,但要排隊上地鐵:I/O阻塞

上了地鐵,發現暫時沒座位:synchronized阻塞

地鐵上找到座位:Running

到達目的地:Dead

2.3 notify()鎖不釋放

<font color="red">當方法wait()被執行後,鎖自動被釋放,但執行完notify()方法後,鎖不會自動釋放。必須執行完notify()方法所在的synchronized代碼塊後才釋放。</font>

下面我們通過代碼驗證一下:

(完整代碼:https://github.com/Snailclimb/threadDemo/tree/master/src/wait\_notifyHoldLock

<font size="2">帶wait方法的synchronized代碼塊</font>

            synchronized (lock) {
                System.out.println("begin wait() ThreadName="
                        + Thread.currentThread().getName());
                lock.wait();
                System.out.println("  end wait() ThreadName="
                        + Thread.currentThread().getName());
            }

<font size="2">帶notify方法的synchronized代碼塊</font>

            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代碼塊。

<font size="2">運行效果如下:</font>

運行效果

<font color="red">這也驗證了我們剛開始的結論:必須執行完notify()方法所在的synchronized代碼塊後才釋放。</font>

2.4 當interrupt方法遇到wait方法

<font color="red">當線程呈wait狀態時,對線程對象調用interrupt方法會出現InterrupedException異常。</font>

<font size="2">Service.java</font>

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了!");
        }
    }
}

<font size="2">ThreadA.java</font>

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);
    }

}

<font size="2">Test.java</font>

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();
        }

    }

}

<font size="2">運行結果:</font>

運行結果

參考:

《Java多線程編程核心技術》

《Java併發編程的藝術》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章