天天看點

java并發系列(3)——線程協作:wait,notify,join

接上一篇:《java并發系列(2)——線程共享,synchronized與volatile》

文章目錄

      • 2.4 線程協作/通信
        • 2.4.1 wait/notify
          • 2.4.1.1 monitor 回顧
          • 2.4.1.2 wait/notify 的作用
          • 2.4.1.3 wait/notify 的标準使用範式
          • 2.4.1.4 wait,sleep,yield
          • 2.4.1.5 wait 阻塞,sleep 阻塞與 synchronized 阻塞
          • 2.4.1.6 notify 與 notifyAll
        • 2.4.2 join

2.4 線程協作/通信

2.4.1 wait/notify

首先要注意的是:wait,notify,notifyAll 這三個方法跟 sleep,yield,join,interrupt,suspend,resume,stop 這些不一樣,後者是 Thread 特有的方法,前者是 Object 的方法。

2.4.1.1 monitor 回顧

在 wait,notify 之前,先回顧一下 monitor。前面在講 synchronized 時提到 monitor 計數器,以及 monitor 上标記的目前持有鎖的線程。這裡會再增加一個 wait set。

monitor 持有的資訊:

  • 計數器:獲得鎖或重入鎖時,計數器 +1,退出時計數器 -1,計數器為 0 意味着鎖沒有被任何線程獲得;
  • 線程:記錄了目前鎖被哪個線程所持有;
  • wait set:調用了 wait 方法的線程,會被記錄在這裡。(每個 Object 都有自己的 monitor,在哪個 Object 上調用 wait,就會被記錄到哪個 Object 的 monitor 的 wait set。)
2.4.1.2 wait/notify 的作用

概況地說,wait 方法會使目前線程進入阻塞狀态,直到被其它線程使用 notify 或 notifyAll 喚醒。

具體行為如下:

wait:

  • 進入 wait 時:線程釋放 monitor 鎖,進入阻塞狀态,線程被記錄到 wait set;
  • 被 notify 時:線程從 wait set 中被清除,進入就緒狀态等待線程排程,獲得 cpu 使用權開始執行後先競争 monitor 鎖(行為與 synchronized 一樣,并且不會比其它線程更優先競争到鎖),所有同步狀态恢複到調用 wait 方法之前,然後從 wait 方法正常傳回;
  • 被 interrupt 時:線程行為與被 notify 相同,差別是從 wait 方法異常傳回(同時 interrupt 狀态會被清除);
  • 意外醒來:wait 中的線程有極低的機率在沒有逾時,沒有被 notify,沒有被 interrupt 的情況下醒來,這是作業系統導緻的,被稱為“欺騙性喚醒”。

notify:如果 wait set 中有線程,喚醒 wait set 中的一個線程(具體喚醒哪個線程不可控,JVM 可自由實作);如果 wait set 中沒有線程就忽略。

notifyAll:喚醒 wait set 中所有的線程。

2.4.1.3 wait/notify 的标準使用範式
//wait
synchronized (object) {
    while (<condition>) {
        object.wait();
        //object.wait(timeout);
    }
    //...
}

//notify
synchronized (object) {
    //...
    object.notify();
    //object.notifyAll();
}
           
  • wait,notify,notifyAll 方法都需要先獲得鎖(在哪個對象上執行方法,就需要獲得哪個對象的鎖);
  • wait 方法要放在循環裡面執行(當被喚醒時檢查是否滿足放行條件,不滿足就繼續 wait),防止被意外喚醒;
  • notify 方法盡量在最後執行(要保證 notify 執行完後盡快釋放鎖),因為 wait 線程被喚醒後需要獲得鎖,notify 線程如果不釋放鎖,即使喚醒了 wait 線程,wait 線程也還是會阻塞。

wait,notify 示例代碼見《利用wait/notify模拟消息隊列》

2.4.1.4 wait,sleep,yield

三者異同:

  • 都會導緻線程阻塞(如果 yield 沒有被線程排程器忽略);
  • wait 和 sleep 都能被 interrupt;
  • wait 會釋放鎖,sleep 和 yield 不會(順便一提,suspend 也不會);
  • wait 除了逾時蘇醒,還能被喚醒,sleep 隻能逾時蘇醒;
  • wait 由于必須先獲得鎖,是以會伴随着線程工作記憶體的重新整理,而 sleep 和 yield 都沒有規定必須重新整理記憶體。
2.4.1.5 wait 阻塞,sleep 阻塞與 synchronized 阻塞

從作業系統層面講,都是阻塞狀态,阻塞狀态的線程都不會被排程。

在 Java 層面,wait 對應的線程狀态是 Thread.State.WAITING 或者 Thread.State.TIMED_WAITING,取決于是否設定了逾時。

sleep 阻塞對應的線程狀态是 Thread.State.TIMED_WAITING,因為 sleep 必須設定逾時時間。

synchronized 阻塞對應的線程狀态是 Thread.State.BLOCKED,這個狀态的阻塞是因為鎖競争而導緻的。

是以,wait 可能會産生兩種阻塞狀态,在醒來之前是 WAITING 或 TIMED_WAITING,醒來之後會競争鎖,如果沒有競争到鎖則可能會進入 BLOCKED 狀态。

另外,synchronized 未必會導緻線程阻塞,即使沒有競争到鎖。Java 為了減少線程切換開銷,在沒有競争到鎖的情況下有可能讓線程空跑(自旋狀态,類似于跑一個空的死循環)而非進入阻塞狀态。

2.4.1.6 notify 與 notifyAll

使用 notify 不會出現問題的情況下盡量使用 notify,而不是 notifyAll。

因為 notifyAll 會喚醒多個線程,喚醒的線程都要競争鎖,必然隻有一個線程得到鎖,其它被喚醒的線程又會重新進入阻塞狀态,增加了不必要的線程切換開銷。

适合使用 notify 的情況:

Thread a = new Thread(() -> {
    synchronized (monitor) {
        while (conditionA()) {
            monitor.wait();
        }
        System.out.println("A");
    }
});
Thread b = new Thread(() -> {
    synchronized (monitor) {
        while (conditionA()) {
            monitor.wait();
        }
        System.out.println("A");
    }
});
           

這兩個線程,醒來之後做的事情是一樣的,喚醒誰都無所謂,使用 notify 就可以。

如果再加一個這樣的線程:

Thread c = new Thread(() -> {
    synchronized (monitor) {
        while (conditionB()) {
            monitor.wait();
        }
        System.out.println("B");
    }
});
           

這個線程醒來後幹的事情不一樣,那麼使用 notify 喚醒的線程可能不符合預期,這時候就隻能用 notifyAll,不符合預期的線程因為不滿足退出 while 循環條件會重新 wait。

當然,如果不在乎這點線程切換開銷,反而更擔心代碼運作出錯,那麼全都 notifyAll 也不是不行。

2.4.2 join

join 方法的作用可以概括為“線程插隊”,即阻塞目前線程,讓其它線程先執行,等其它線程執行完或逾時,目前線程再恢複執行。

join 方法是用 wait 方法實作的,是以 wait 方法可以被 interrupt,join 方法自然也可以。

當然 join 方法也必須獲得線程對象的鎖。

可以看下它的實作:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
           

核心邏輯也就是下面這三行代碼(else 裡面增加了對逾時時間的控制,本質一樣):

while (isAlive()) {
                wait(0);
            }
           

wait(0) 等同于 wait() ,即沒有逾時時間。

這三行代碼的意思就是: 如果 join 進來的線程還活着(即已經調了 start 方法但還沒有執行完成),就一直等。

不用擔心這裡的 wait 會永遠等待下去,因為線程終止的時候,會調用 this.notifyAll 方法。

可以寫幾行代碼測試一下:

package per.lvjc.concurrent.waitnotify;

import java.util.concurrent.TimeUnit;

public class ThreadTerminatedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("thread begin");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread end");
        });
        thread.start();
        synchronized (thread) {
            thread.wait();
        }
        System.out.println("main end");
    }
}
           

在 Thread 對象上 wait。跑一下可以發現 thread 執行完之後,wait 是會被喚醒的。