歡迎光臨
每天分享高質量文章

Java 執行緒的 wait 和 notify 的神坑

  • 問題一:通知丟失
    • 問題分析
  • 問題二:假喚醒
  • 等待/通知的典型正規化
    • 等待方遵循原則
    • 通知方遵循原則

也許我們只知道wait和notify是實現執行緒通訊的,同時要使用synchronized包住,其實在開發中知道這個是遠遠不夠的。接下來看看兩個常見的問題。

問題一:通知丟失

建立2個執行緒,一個執行緒負責計算,一個執行緒負責獲取計算結果。

public class Calculator extends Thread {
    int total;

    @Override
    public void run() {
        synchronized (this){
            for(int i = 0; i 101; i++){
                total += i;
            }
            this.notify();
        }

    }
}

public class ReaderResult extends Thread {
    Calculator c;
    public ReaderResult(Calculator c) {
        this.c = c;
    }

    @Override
    public void run() {
        synchronized (c) {
            try {
                System.out.println(Thread.currentThread() + "等待計算結...");
                c.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
        }
    }


    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        //先啟動獲取計算結果執行緒
        new ReaderResult(calculator).start();
        calculator.start();

    }
}

我們會獲得預期的結果:
Thread[Thread-1,5,main]等待計算結...
Thread[Thread-1,5,main]計算結果為:5050

但是我們修改為先啟動計算執行緒呢?
calculator.start();
new ReaderResult(calculator).start();

這是獲取結算結果執行緒一直等待:
Thread[Thread-1,5,main]等待計算結...

問題分析

打印出執行緒堆疊:

"Thread-1" prio=5 tid=0x00007f983b87e000 nid=0x4d03 in Object.wait() [0x0000000118988000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)
    at java.lang.Object.wait(Object.java:503)
    at com.concurrent.waitnotify.ReaderResult.run(ReaderResult.java:18)
    - locked <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)

可以看出ReaderResult在Calculator上等待。發生這個現象就是常說的通知丟失,在獲取通知前,通知提前到達,我們先計算結果,計算完後再通知,但是這個時候獲取結果沒有在等待通知,等到獲取結果的執行緒想獲取結果時,這個通知已經通知過了,所以就發生丟失,那我們該如何避免?可以設定變數表示是否被通知過,修改程式碼如下:

public class Calculator extends Thread {
    int total;
    boolean isSignalled = false;

    @Override
    public void run() {
        synchronized (this) {
            isSignalled = true;//已經通知過
                for (int i = 0; i 101; i++) {
                    total += i;
                }
                this.notify();
            }
    }
}

public class ReaderResult extends Thread {

    Calculator c;

    public ReaderResult(Calculator c) {
        this.c = c;
    }

    @Override
    public void run() {
        synchronized (c) {
            if (!c.isSignalled) {//判斷是否被通知過
                try {
                    System.out.println(Thread.currentThread() + "等待計算結...");
                    c.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
            }

        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        new ReaderResult(calculator).start();
        calculator.start();
    }
}

問題二:假喚醒

兩個執行緒去刪除陣列的元素,當沒有元素的時候等待,另一個執行緒新增一個元素,新增完後通知刪除資料的執行緒。

public class EarlyNotify{
    private List list;

    public EarlyNotify() {
        list = Collections.synchronizedList(new LinkedList());
    }

    public String removeItem() throws InterruptedException {

        synchronized ( list ) {
            if ( list.isEmpty() ) {  //問題在這
                list.wait();
            }

            //刪除元素
            String item = (String) list.remove(0);
            return item;
        }
    }

    public void addItem(String item) {
        synchronized ( list ) {
            //新增元素
            list.add(item);
            //新增後,通知所有執行緒
            list.notifyAll();
        }
    }

    private static void print(String msg) {
        String name = Thread.currentThread().getName();
        System.out.println(name + ": " + msg);
    }

    public static void main(String[] args) {
        final EarlyNotify en = new EarlyNotify();

        Runnable runA = new Runnable() {
            public void run() {
                try {
                    String item = en.removeItem();

                } catch ( InterruptedException ix ) {
                    print("interrupted!");
                } catch ( Exception x ) {
                    print("threw an Exception!!!\n" + x);
                }
            }
        };

        Runnable runB = new Runnable() {
            public void run() {
                en.addItem("Hello!");
            }
        };

        try {
            //啟動第一個刪除元素的執行緒
            Thread threadA1 = new Thread(runA, "threadA1");
            threadA1.start();

            Thread.sleep(500);

            //啟動第二個刪除元素的執行緒
            Thread threadA2 = new Thread(runA, "threadA2");
            threadA2.start();

            Thread.sleep(500);
            //啟動增加元素的執行緒
            Thread threadB = new Thread(runB, "threadB");
            threadB.start();

            Thread.sleep(1000); // wait 10 seconds

            threadA1.interrupt();
            threadA2.interrupt();
        } catch ( InterruptedException x ) {}
    }
}

結果:
threadA1: threw an Exception!!!
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

這裡發生了假喚醒,當新增完一個元素然後喚醒兩個執行緒去刪除,這個只有一個元素,所以會丟擲陣列越界,這時我們需要喚醒的時候在判斷一次是否還有元素。
修改程式碼:

  public String removeItem() throws InterruptedException {

        synchronized ( list ) {
            while ( list.isEmpty() ) {  //問題在這
                list.wait();
            }

            //刪除元素
            String item = (String) list.remove(0);
            return item;
        }
    }

等待/通知的典型正規化

從上面的問題我們可歸納出等待/通知的典型正規化。該正規化分為兩部分,分別針對等待方(消費者)和通知方(生產者)。

等待方遵循原則如下:

  1. 獲取物件的鎖
  2. 如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件
  3. 條件滿足則執行對應的邏輯

對應偽程式碼如下:

synchronized(物件){
    while(條件不滿足){
        物件.wait();
    }
    對應的處理邏輯
}

通知方遵循原則如下:

  1. 獲得物件的鎖
  2. 改變條件
  3. 通知所以等待在物件上的執行緒

對應偽程式碼如下:

synchronized(物件){
    改變條件
    物件.notifyAll();
}
贊(0)

分享創造快樂