接上一篇:《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 是會被喚醒的。