Java 非同步程式設計之:notify 和 wait 用法

沉靜發表於2019-01-19

最近看帖子,發現一道面試題:

啟動兩個執行緒, 一個輸出 1,3,5,7…99, 另一個輸出 2,4,6,8…100 最後 STDOUT 中按序輸出 1,2,3,4,5…100

題目要求用 Java 的 wait + notify 機制來實現,重點考察對於多執行緒可見性的理解。

wait 和 notify 簡介

wait 和 notify 均為 Object 的方法:

  • Object.wait() —— 暫停一個執行緒
  • Object.notify() —— 喚醒一個執行緒

從以上的定義中,我們可以瞭解到以下事實:

  • 想要使用這兩個方法,我們需要先有一個物件 Object。
  • 在多個執行緒之間,我們可以通過呼叫同一個物件wait()notify()來實現不同的執行緒間的可見。

物件控制權(monitor)

在使用 wait 和 notify 之前,我們需要先了解物件的控制權(monitor)。在 Java 中任何一個時刻,物件的控制權只能被一個執行緒擁有。如何理解控制權呢?請先看下面的簡單程式碼:

public class ThreadTest {
    public static void main(String[] args) {
        Object object = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

直接執行,我們將會得到以下異常:

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at com.xiangyu.demo.ThreadTest$1.run(ThreadTest.java:10)
    at java.lang.Thread.run(Thread.java:748)

出錯的程式碼在:object.wait();。這裡我們需要了解以下事實:

  • 無論是執行物件的 wait、notify 還是 notifyAll 方法,必須保證當前執行的執行緒取得了該物件的控制權(monitor)
  • 如果在沒有控制權的執行緒裡執行物件的以上三種方法,就會報 java.lang.IllegalMonitorStateException 異常。
  • JVM 基於多執行緒,預設情況下不能保證執行時執行緒的時序性

在上面的示例程式碼中,我們 new 了一個 Thread,但是物件 object 的控制權仍在主執行緒裡。所以會報 java.lang.IllegalMonitorStateException 。

我們可以通過同步鎖來獲得物件控制權,例如:synchronized 程式碼塊。對以上的示例程式碼做改造:

public class ThreadTest {
    public static void main(String[] args) {
        Object object = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object){ // 修改處
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

再次執行,程式碼不再報錯。

我們可以得到以下結論:

  • 呼叫物件的wait()notify()方法,需要先取得物件的控制權
  • 可以使用synchronized (object)來取得對於 object 物件的控制權

解題

瞭解了物件控制權之後,我們就可以正常地使用 notify 和 wait 了,下面給出我的解題方法,供參考。

public class ThreadTest {
    private final Object flag = new Object();

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        ThreadA threadA = threadTest.new ThreadA();
        threadA.start();
        ThreadB threadB = threadTest.new ThreadB();
        threadB.start();
    }

    class ThreadA extends Thread {
        @Override
        public void run() {
            synchronized (flag) {
                for (int i = 0; i <= 100; i += 2) {
                    flag.notify();
                    System.out.println(i);
                    try {
                        flag.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }

    class ThreadB extends Thread {
        @Override
        public void run() {
            synchronized (flag) {
                for (int i = 1; i < 100; i += 2) {
                    flag.notify();
                    System.out.println(i);
                    try {
                        flag.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

發散:notify()notifyAll()

這兩個方法均為 native 方法,在JDK 1.8 中的關於notify()的JavaDoc如下:

Wakes up a single thread that is waiting on this object`s monitor. If any threads are waiting on this object, one of them is chosen to be awakened.

譯為:

喚醒此 object 控制權下的一個處於 wait 狀態的執行緒。若有多個執行緒處於此 object 控制權下的 wait 狀態,只有一個會被喚醒。

也就是說,如果有多個執行緒在 wait 狀態,我們並不知道哪個執行緒會被喚醒。

在JDK 1.8 中的關於notifyAll()的JavaDoc如下:

Wakes up all threads that are waiting on this object`s monitor.

譯為:

喚醒所有處於此 object 控制權下的 wait 狀態的執行緒。

所以,我們需要根據實際的業務場景來考慮如何使用。

相關文章