wait、notify、notifyAll的理解與使用

基礎知識

Java 中,可以通過配合調用 Object 對象的 wait() 方法和 notify() 方法或 notifyAll() 方法來實現線程間的通信。
在線程中調用 wait() 方法,將阻塞當前線程,直至等到其他線程調用了調用 notify() 方法或 notifyAll() 方法進行通知之後,當前線程才能從 wait() 方法出返回,繼續執行下面的操作。

wait

該方法用來將當前線程置入休眠狀態,直到接到通知或被中斷爲止。在調用 wait()之前,線程必須要獲得該對象的對象監視器鎖,即只能在同步方法或同步塊中調用 wait()方法。調用 wait()方法之後,當前線程會釋放鎖。如果調用 wait()方法時,線程並未獲取到鎖的話,則會拋出 IllegalMonitorStateException異常,這是一個 RuntimeException。如果再次獲取到鎖的話,當前線程才能從 wait()方法處成功返回。

notify

該方法也要在同步方法或同步塊中調用,即在調用前,線程也必須要獲得該對象的對象級別鎖,如果調用 notify()時沒有持有適當的鎖,也會拋出 IllegalMonitorStateException
該方法任意從 WAITTING 狀態的線程中挑選一個進行通知,使得調用 wait()方法的線程從等待隊列移入到同步隊列中,等待有機會再一次獲取到鎖,從而使得調用 wait()方法的線程能夠從 wait()方法處退出。調用 notify 後,當前線程不會馬上釋放該對象鎖,要等到程序退出同步塊後,當前線程纔會釋放鎖。

notifyAll

該方法與 notify ()方法的工作方式相同,重要的一點差異是:
notifyAll 使所有原來在該對象上 wait 的線程統統退出 WAITTING 狀態,使得他們全部從等待隊列中移入到同步隊列中去,等待下一次能夠有機會獲取到對象監視器鎖。

wait()與notify()操作會釋放鎖嗎?

先說結論:
wait()會立即釋放對象的鎖
notify() 不會立即釋放鎖 當執行完同步代碼塊就會釋放對象的鎖

我們通過以下代碼實驗一下:
ThreadWait類:

class ThreadWait {

    private Object lock;

    public ThreadWait(Object lock) {
        this.lock = lock;
    }

    public void testWait() {
        try {
            synchronized (lock) {
                System.out.println("start wait........");
                lock.wait();
                System.out.println("end wait........");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

先實驗一下Wait() 測試類:

public class ThreadWaitNotifyLockDemo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread waitThread2 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        waitThread1.start();
        waitThread2.start();
    }
}

輸出結果:
在這裏插入圖片描述
說明: 在上面的例子中,我們啓動兩個線程,都去執行線程等待的操作,從執行結果看到,輸出了兩條“start wait”,這個可以說明,wait()操作會立即釋放掉當前持有對象的鎖,否則第二個線程根本不會進入代碼塊中執行。

OK,我們通過上面的例子得到結論,wait()操作會立即釋放其持有的對象鎖,那麼notify()操作是否也是一樣的呢?我們再通過一個例子來實驗一下:
ThreadNotify 類

public class ThreadNotify {

    private Object lock;

    public ThreadNotify(Object lock) {
        this.lock = lock;
    }

    public void testNotify() {
        try {
            synchronized (lock) {
                System.out.println("start notify........" + Thread.currentThread().getName());
                lock.notify();
                //線程休息兩秒
                Thread.sleep(2000);
                System.out.println("end notify........" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試類:

public class ThreadWaitNotifyLock2Demo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread notifyThread1 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        Thread notifyThread2 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        Thread notifyThread3 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        waitThread1.start();
        notifyThread1.start();
        notifyThread2.start();
        notifyThread3.start();
    }
}

輸出結果:
在這裏插入圖片描述
說明:這個例子中,我們啓動了四個線程,第一個線程執行等待操作,其他兩個線程執行喚醒操作,從執行結果中可以看到,當第一次notify後,線程休息了2秒,如果notify立即釋放了鎖,那麼在其sleep的時候,必然會有其他線程爭搶到鎖並執行,但是從結果中,可以看到這並沒有發生,由此可以說明notify()操作不會立即釋放其持有的對象鎖,而是 當執行完同步代碼塊就會釋放對象的鎖

wait/notify 消息通知潛在的一些問題

notify 早期通知

notify 通知的遺漏很容易理解,即 threadA 還沒開始 wait 的時候,threadB 已經 notify 了,這樣,threadB 通知是沒有任何響應的,當 threadB 退出 synchronized 代碼塊後,threadA 再開始 wait,便會一直阻塞等待,直到被別的線程打斷。
比如在下面的示例代碼中,就模擬出 notify 早期通知帶來的問題:

public class EarlyNotify {
    private static String lockObject = "";

    public static void main(String[] args) {
        WaitThread waitThread = new WaitThread(lockObject);
        NotifyThread notifyThread = new NotifyThread(lockObject);
        notifyThread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        waitThread.start();
    }

    static class WaitThread extends Thread {
        private String lock;

        public WaitThread(String lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread().getName() + "  進去代碼塊");
                    System.out.println(Thread.currentThread().getName() + "  開始wait");
                    lock.wait();
                    System.out.println(Thread.currentThread().getName() + "   結束wait");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class NotifyThread extends Thread {
        private String lock;

        public NotifyThread(String lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "  進去代碼塊");
                System.out.println(Thread.currentThread().getName() + "  開始notify");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + "   結束開始notify");
            }
        }
    }
}

輸出結果:
在這裏插入圖片描述
示例中開啓了兩個線程,一個是 WaitThread,另一個是 NotifyThread。NotifyThread 會先啓動,先調用 notify 方法。然後 WaitThread 線程才啓動,調用 wait 方法,但是由於通知過了,wait 方法就無法再獲取到相應的通知,因此 WaitThread 會一直在 wait 方法出阻塞,這種現象就是通知過早的現象。
解決方法:一般是添加一個狀態標誌,讓 waitThread 調用 wait 方法前先判斷狀態是否已經改變了沒,如果通知早已發出的話,WaitThread 就不再去 wait。對上面的代碼進行更正:

public class EarlyNotify {
    private static String lockObject = "";
    private static boolean isWait = true;

    public static void main(String[] args) {
        WaitThread waitThread = new WaitThread(lockObject);
        NotifyThread notifyThread = new NotifyThread(lockObject);
        notifyThread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        waitThread.start();
    }

    static class WaitThread extends Thread {
        private String lock;

        public WaitThread(String lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                try {
                    while (isWait) {
                        System.out.println(Thread.currentThread().getName() + "  進去代碼塊");
                        System.out.println(Thread.currentThread().getName() + "  開始wait");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName() + "   結束wait");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class NotifyThread extends Thread {
        private String lock;

        public NotifyThread(String lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "  進去代碼塊");
                System.out.println(Thread.currentThread().getName() + "  開始notify");
                lock.notifyAll();
                isWait = false;
                System.out.println(Thread.currentThread().getName() + "   結束開始notify");
            }
        }
    }
}

輸出結果:
在這裏插入圖片描述
解析:這段代碼只是增加了一個 isWait 狀態變量,NotifyThread 調用 notify 方法後會對狀態變量進行更新,在 WaitThread 中調用 wait 方法之前會先對狀態變量進行判斷,在該示例中,調用 notify 後將狀態變量isWait改變爲 false,因此,在 WaitThread 中 while 對 isWait 判斷後就不會執行 wait 方法,從而避免了 Notify 過早通知造成遺漏的情況。
總結:在使用線程的等待/通知機制時,一般都要配合一個 boolean 變量值(或者其他能夠判斷真假的條件),在 notify 之前改變該 boolean 變量的值,讓 wait 返回後能夠退出 while 循環(一般都要在 wait 方法外圍加一層 while 循環,以防止早期通知),或在通知被遺漏後,不會被阻塞在 wait 方法處。這樣便保證了程序的正確性。

等待 wait 的條件發生變化

如果線程在等待時接受到了通知,但是之後等待的條件發生了變化,並沒有再次對等待條件進行判斷,也會導致程序出現錯誤。

下面用一個例子來說明這種情況

import java.util.ArrayList;
import java.util.List;

public class ConditionChange {
    private static List<String> lockObject = new ArrayList();
    public static void main(String[] args) {
        Consumer consumer1 = new Consumer(lockObject);
        Consumer consumer2 = new Consumer(lockObject);
        Productor productor = new Productor(lockObject);
        consumer1.start();
        consumer2.start();
        productor.start();
    }
    static class Consumer extends Thread {
        private List<String> lock;
        public Consumer(List lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                try {
                    //這裏使用if的話,就會存在wait條件變化造成程序錯誤的問題
                    if (lock.isEmpty()) {
                        System.out.println(Thread.currentThread().getName() + " list爲空");
                        System.out.println(Thread.currentThread().getName() + " 調用wait方法");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName() + "  wait方法結束");
                    }
                    String element = lock.remove(0);
                    System.out.println(Thread.currentThread().getName() +
                            " 取出第一個元素爲:" + element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    static class Productor extends Thread {
        private List<String> lock;
        public Productor(List lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " 開始添加元素");
                lock.add(Thread.currentThread().getName());
                lock.notifyAll();
            }
        }

    }
}

輸出結果:
在這裏插入圖片描述
異常原因分析:在這個例子中一共開啓了 3 個線程,Consumer1,Consumer2 以及 Productor。首先 Consumer1 調用了 wait 方法後,線程處於了 WAITTING 狀態,並且將對象鎖釋放出來。因此,Consumer2 能夠獲取對象鎖,從而進入到同步代塊中,當執行到 wait 方法時,同樣的也會釋放對象鎖。因此,productor 能夠獲取到對象鎖,進入到同步代碼塊中,向 list 中插入數據後,通過 notifyAll 方法通知處於 WAITING 狀態的 Consumer1 和 Consumer2 線程。consumer1 得到對象鎖後,從 wait 方法出退出,刪除了一個元素讓 List 爲空,方法執行結束,退出同步塊,釋放掉對象鎖。這個時候 Consumer2 獲取到對象鎖後,從 wait 方法退出,繼續往下執行,這個時候 Consumer2 再執行lock.remove(0);就會出錯,因爲 List 由於 Consumer1 刪除一個元素之後已經爲空了。
解決方案:通過上面的分析,可以看出 Consumer2 報異常是因爲線程從 wait 方法退出之後沒有再次對 wait 條件進行判斷,因此,此時的 wait 條件已經發生了變化。解決辦法就是,在 wait 退出之後再對條件進行判斷即可。
將 wait 外圍的 if 語句改爲 while 循環即可,這樣當 list 爲空時,線程便會繼續等待,而不會繼續去執行刪除 list 中元素的代碼。

while (lock.isEmpty()) {}

**總結:**在使用線程的等待/通知機制時,一般都要在 while 循環中調用 wait()方法,因此需要配合使用一個 boolean 變量(或其他能判斷真假的條件,如本文中的 list.isEmpty()),滿足 while 循環的條件時,進入 while 循環,執行 wait()方法,不滿足 while 循環的條件時,跳出循環,執行後面的代碼。

“假死”狀態

現象:如果是多消費者和多生產者情況,如果使用 notify 方法可能會出現“假死”的情況,即喚醒的是同類線程。
原因分析:假設當前多個生產者線程會調用 wait 方法阻塞等待,當其中的生產者線程獲取到對象鎖之後使用 notify 通知處於 WAITTING 狀態的線程,如果喚醒的仍然是生產者線程,就會造成所有的生產者線程都處於等待狀態。
解決辦法:將 notify 方法替換成 notifyAll 方法,如果使用的是 lock 的話,就將 signal 方法替換成 signalAll 方法。

總結

  1. wait( ),notify( ),notifyAll()都不屬於Thread類,而是屬於Object基礎類,也就是每個對象都有wait( ),notify( ),notifyAll( )的功能,因爲每個對象都有鎖,鎖是每個對象的基礎,當然操作鎖的方法也是最基礎了。
  2. 當需要調用以上的方法的時候,一定要對競爭資源進行加鎖,如果不加鎖的話,則會報 IllegalMonitorStateException異常
  3. 當想要調用wait( )進行線程等待時,必須要取得這個鎖對象的控制權(對象監視器),一般是放到synchronized(obj)代碼中。
  4. obj.wait()方法返回後,線程A需要再次獲得obj鎖,才能繼續執行。
  5. 在while循環裏而不是 if 語句下使用wait,這樣,會在線程暫停恢復後都檢查wait的條件,並在條件實際上並未改變的情況下處理喚醒通知
  6. 調用obj.wait( )釋放了obj的鎖,否則其他線程也無法獲得obj的鎖,也就無法在synchronized(obj){obj.notify() } 代碼段內喚醒A。
  7. 假設有三個線程執行了obj.wait( ),那麼obj.notifyAll()則能全部喚醒tread1,thread2,thread3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,因此,tread1,thread2,thread3只有一個有機會獲得鎖繼續執行,具體執行哪個線程由JVM決定,例如tread1,其餘的需要等待thread1釋放obj鎖之後才能繼續執行。
  8. 當調用obj.notify/notifyAll後,調用線程依舊持有obj鎖,因此,thread1,thread2,thread3雖被喚醒,但是仍無法獲得obj鎖。直到調用線程退出synchronized塊,釋放obj鎖後,thread1,thread2,thread3中的一個纔有機會獲得鎖繼續執行。

應用實例

生產者-消費者模型的實現

  • 使用Object的wait()和notify()實現
public class Test {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = 
    	new PriorityQueue<Integer>(queueSize);
      
    public static void main(String[] args)  {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
          
        producer.start();
        consumer.start();
    }
      
    class Consumer extends Thread{
          
        @Override
        public void run() {
            consume();
        }
          
        private void consume() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == 0){
                        try {
                            System.out.println("隊列空,等待數據");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.poll();          //每次移走隊首元素
                    queue.notify();
                    System.out.println("從隊列取走一個元素,隊列剩餘"+
                    		queue.size()+"個元素");
                }
            }
        }
    }
      
    class Producer extends Thread{
          
        @Override
        public void run() {
            produce();
        }
          
        private void produce() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == queueSize){
                        try {
                            System.out.println("隊列滿,等待有空餘空間");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.offer(1);        //每次插入一個元素
                    queue.notify();
                    System.out.println("向隊列取中插入一個元素,隊列剩餘空間:"+
                    		(queueSize-queue.size()));
                }
            }
        }
    }
}

輸出結果:只截取了一次循環的結果
在這裏插入圖片描述
參考:

  1. https://blog.csdn.net/jianiuqi/article/details/53448849
  2. https://www.cnblogs.com/dolphin0520/p/3920385.html
  3. https://juejin.im/post/5aeec675f265da0b7c072c56#heading-1
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章