Java併發(二十二)----wait notify的正確姿勢

|舊市拾荒|發表於2024-01-11

開始之前先看看,sleep(long n)wait(long n) 的區別:

1) sleep 是 Thread 的靜態方法,而 wait 是 Object 的方法

2) sleep 不需要強制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

3) sleep 在睡眠的同時,不會釋放物件鎖的,但 wait 在等待的時候會釋放物件鎖

4) 它們狀態 TIMED_WAITING

建議:鎖物件加final修飾,這樣鎖物件不可變。

改進 1

static final Object room = new Object();
static boolean hasCigarette = false;  // 是否有煙
static boolean hasTakeout = false;

思考下面的解決方案好不好,為什麼?

new Thread(() -> {
    synchronized (room) {
        log.debug("有煙沒?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("沒煙,先歇會!");
            sleep(2);
        }
        log.debug("有煙沒?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以開始幹活了");
        }
    }
}, "小南").start();
​
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("可以開始幹活了");
        }
    }, "其它人").start();
}
​
sleep(1);
new Thread(() -> {
    // 這裡能不能加 synchronized (room)?  不能,sleep不會釋放物件鎖 
    hasCigarette = true;
    log.debug("煙到了噢!");
}, "送煙的").start();

輸出

20:49:49.883 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:49:49.887 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:49:50.882 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:49:51.887 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:49:51.887 [小南] c.TestCorrectPosture - 可以開始幹活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
  • 其它幹活的執行緒,都要一直阻塞,效率太低

  • 小南執行緒必須睡足 2s 後才能醒來,就算煙提前送到,也無法立刻醒來

  • 加了 synchronized (room) 後,就好比小南在裡面反鎖了門睡覺,煙根本沒法送進門,main 沒加 synchronized 就好像 main 執行緒是翻窗戶進來的

  • 解決方法,使用 wait - notify 機制

改進 2

思考下面的實現行嗎,為什麼?

new Thread(() -> {
    synchronized (room) {
        log.debug("有煙沒?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("沒煙,先歇會!");
            try {
                room.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("有煙沒?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以開始幹活了");
        }
    }
}, "小南").start();
​
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("可以開始幹活了");
        }
    }, "其它人").start();
}
​
sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasCigarette = true;
        log.debug("煙到了噢!");
        room.notify();
    }
}, "送煙的").start();

輸出

20:51:42.489 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:51:42.493 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:43.490 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:51:43.490 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:51:43.490 [小南] c.TestCorrectPosture - 可以開始幹活了
  • 解決了其它幹活的執行緒阻塞的問題

  • 但如果有其它執行緒也在等待條件呢?

改進 3

new Thread(() -> {
    synchronized (room) {
        log.debug("有煙沒?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("沒煙,先歇會!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("有煙沒?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以開始幹活了");
        } else {
            log.debug("沒幹成活...");
        }
    }
}, "小南").start();
​
new Thread(() -> {
    synchronized (room) {
        Thread thread = Thread.currentThread();
        log.debug("外賣送到沒?[{}]", hasTakeout);
        if (!hasTakeout) {
            log.debug("沒外賣,先歇會!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("外賣送到沒?[{}]", hasTakeout);
        if (hasTakeout) {
            log.debug("可以開始幹活了");
        } else {
            log.debug("沒幹成活...");
        }
    }
}, "小女").start();
​
sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外賣到了噢!");
        room.notify();
    }
}, "送外賣的").start();

輸出

20:53:12.173 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:53:12.176 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:53:12.176 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:53:12.176 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:53:13.174 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:53:13.174 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:53:13.174 [小南] c.TestCorrectPosture - 沒幹成活...
  • notify 只能隨機喚醒一個 WaitSet 中的執行緒,這時如果有其它執行緒也在等待,那麼就可能喚醒不了正確的執行緒,稱之為【虛假喚醒】

  • 解決方法,改為 notifyAll

改進 4

new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外賣到了噢!");
        room.notifyAll();
    }
}, "送外賣的").start();

輸出

20:55:23.978 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:55:23.982 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:55:23.982 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:55:23.982 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:55:24.979 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:55:24.979 [小女] c.TestCorrectPosture - 外賣送到沒?[true]
20:55:24.980 [小女] c.TestCorrectPosture - 可以開始幹活了
20:55:24.980 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:55:24.980 [小南] c.TestCorrectPosture - 沒幹成活...
  • 用 notifyAll 僅解決某個執行緒的喚醒問題,但使用 if + wait 判斷僅有一次機會,一旦條件不成立,就沒有重新判斷的機會了

  • 解決方法,用 while + wait,當條件不成立,再次 wait

改進 5

將 if 改為 while

if (!hasCigarette) {
    log.debug("沒煙,先歇會!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

改動後

while (!hasCigarette) {
    log.debug("沒煙,先歇會!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

輸出

20:58:34.322 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:58:34.326 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:58:34.326 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:58:34.326 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:58:35.323 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:58:35.324 [小女] c.TestCorrectPosture - 外賣送到沒?[true]
20:58:35.324 [小女] c.TestCorrectPosture - 可以開始幹活了
20:58:35.324 [小南] c.TestCorrectPosture - 沒煙,先歇會!

while迴圈 + wait 防止虛假喚醒,並且一般使用notifyAll來進行喚醒

synchronized(lock) {
    while(條件不成立) {
        lock.wait();
    }
    // 幹活
}
​
//另一個執行緒
synchronized(lock) {
    lock.notifyAll();
}