1. 程式人生 > 其它 >[java併發]深入淺出條件佇列-wait、notify、notifyall

[java併發]深入淺出條件佇列-wait、notify、notifyall

技術標籤:javajava併發程式設計多執行緒佇列thread

君子生非異也,善假於物也 —— [荀子]·[勸學]

一、導言

條件佇列靈活,但用錯也十分容易。一般來說能用BlockingQueueLatchSemaphoreFuture等高階工具實現的就不要直接使用條件佇列。 ——<<java併發程式設計實戰>>

java的內建的條件佇列存在一些缺陷,每個內建鎖(基於synchronize塊)都只能有一個關聯的條件佇列,因此可能存在多個執行緒因不同的條件謂詞不滿足而在同一個條件佇列上。這個特性很可能就會導致"通知丟失"(不使用notifyall下)。就算使用了notifyall也會因為鎖的爭搶導致一些無謂的cpu資源的浪費。

二、基本使用

ps: 本篇博文程式碼使用了lombok@SneakThrow註解,作用就是避免各種在方法頭上的異常宣告

2.1. wait()

wait()方法會自動釋放鎖,進入等待佇列,直到被喚醒。執行緒被喚醒後會重新請求鎖,它和其他嘗試進入synchronize塊的執行緒沒有區別。

2.1.1. 不持有當前鎖呼叫wait會報錯

@Test
@SneakyThrows
public void waitSt() {
    // 必須在synchronized程式碼塊裡,否則會報錯: java.lang.IllegalMonitorStateException
    synchronized
(this) { wait(); System.out.println("這句話永遠不可能輸出"); } } @Test @SneakyThrows public void waitSt4() { Object o = new Object(); synchronized (o) { //載入不同邏輯塊會報錯 wait(); } }

2.1.2. wait會被中斷喚醒

@Test
@SneakyThrows
public void waitSt2() {
    synchronized
(this) { Thread thread = Thread.currentThread(); Runnable runnable = new Runnable() { @Override @SneakyThrows public void run() { // 睡眠一秒後喚醒main執行緒 Thread.sleep(1_000); thread.interrupt(); } }; new Thread(runnable).start(); wait(); // 被中斷打斷等待,直接報錯退出程式 System.out.println("這句話永遠不可能輸出"); } }

2.1.3. wait被notify喚醒

@Test
@SneakyThrows
public void waitSt3() {
    // 必須在synchronized程式碼塊裡,否則會報錯: java.lang.IllegalMonitorStateException
    synchronized (this) {
        Thread thread = Thread.currentThread();
        Runnable runnable = new Runnable() {
            @Override
            @SneakyThrows
            public void run() {
								// 睡一秒後喚醒main執行緒
                Thread.sleep(1_000);
                synchronized (ThreadST.this) /*必須被synchronized住*/ {
                    ThreadST.this.notify();
                }
            }
        };
        new Thread(runnable).start();

        /*其實被notify喚醒後還有其他的條件判斷,因為被喚醒的可能有多種,還可能在被notify和從wait喚醒之間,狀態又變了*/
        wait(); // 被另一個執行緒notify
        System.out.println("這句話永遠可以輸出");
    }
}


2.1.4 小結

wait()方法的使用會有過早喚醒誤喚醒的問題,可能被其他執行緒notify(),進而去爭鎖、搶佔cpu,但很可能不是因為它預設的喚醒條件而導致的喚醒,所以wait()喚醒後需要進行條件判斷,且wait()方法應該在迴圈中呼叫。

可以這樣比喻: 小明使用程式碼事項app給自己設定了“購物”、“看電影”、“健身”等代辦事項,當代辦事項app響鈴了,提示小明此時有代辦事項,小明應該檢視下是什麼代辦事項。

synchronized(物件){ // 獲取鎖失敗,執行緒會加入到同步佇列中 
	while(條件不滿足){
		物件.wait();// 呼叫wait方法當前執行緒加入到條件佇列中
	}
}

2.2. notify()notifyAll()

一句話總結區別: notifyAll()方法喚醒所有 wait 執行緒, notify()方法只隨機喚醒一個 wait 執行緒。一般來說,使用notifyall()要好於使用notify。僅僅使用notify()可能會導致通知丟失的情況: 通知錯了物件,而真正應該收到通知的沒有接到通知(沒有被喚醒)。

tips: notify後要儘快的釋放鎖,避免從wait()中返回的執行緒阻塞。

2.2.1 使用notifyall和wait實現阻塞佇列

final/*禁止繼承,防止子類誤用導致執行緒不安全*/ class QueueNotifySt<T> {
    private final int maxLength;
    private ArrayList<T> queue;

    public QueueNotifySt(int maxLength) {
        this.maxLength = maxLength;
        queue = new ArrayList<>(maxLength);
    }

    // 放
    @SneakyThrows
    public synchronized void put(T data) {
        // 滿則等待+退出鎖
        while (maxLength == queue.size()) {
            wait();
        }
        queue.add(data);
        notifyAll();
    }

    //取
    @SneakyThrows
    public synchronized T take() {
        // 空則等待+退出鎖
        while (queue.size() == 0) {
            wait();
        }
        T res = queue.remove(0);
        notifyAll();
        return res;
    }
}

測試程式碼如下所示:

@Test
@SneakyThrows
public void takeTest() {
    QueueNotifySt<Integer> queue = new QueueNotifySt<>(3);
    new Thread(() -> queue.put(1)).start();
    System.out.println("queue.take() = " + queue.take()); //輸出1
    queue.take();
    System.out.println("不會輸出");// 不會執行到這一步
}

三、總結

  1. 生產環境永遠在迴圈中呼叫wait()方法
  2. 永遠在呼叫wait()前測試條件謂詞,被notify()或notofyall()喚醒後也要測試條件謂詞,若謂詞不滿足則繼續wait()
  3. 呼叫wait()notify()notifyall()以及條件謂詞判斷時都得持有鎖: 這三個條件佇列依附物件的鎖。

四、參考文章

  1. 淺談Java中的Condition條件佇列,手摸手帶你實現一個阻塞佇列!
  2. java中的notify和notifyAll有什麼區別?
    在這裡插入圖片描述