sleep/wait/notify/notifyAll分別有什么作用?它們的區別是什么?wait時為什么要放在循環里而不能直接用if?
簡介
首先對幾個相關的方法做個簡單解釋,Object中有幾個用於線程同步的方法:wait、notify、notifyAll。
public class Object {
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
}
- wait: 釋放當前鎖,阻塞直到被notify或notifyAll喚醒,或者超時,或者線程被中斷(InterruptedException)
- notify: 任意選擇一個(無法控制選哪個)正在這個對象上等待的線程把它喚醒,其它線程依然在等待被喚醒
- notifyAll: 喚醒所有線程,讓它們去競爭,不過也只有一個能搶到鎖
- sleep: 不是Object中的方法,而是Thread類的靜態方法,讓當前線程持有鎖阻塞指定時間
sleep和wait
sleep和wait都可以讓線程阻塞,也都可以指定超時時間,甚至還都會拋出中斷異常InterruptedException。
而它們最大的區別就在於,sleep時線程依然持有鎖,別人無法進當前同步方法;wait時放棄了持有的鎖,其它線程有機會進入該同步方法。多次提到同步方法,因為wait必須在synchronized同步代碼塊中,否則會拋出異常IllegalMonitorStateException,notify也是如此,可以說wait和notify是就是為了在同步代碼中做線程調度而生的。
下面一個簡單的例子展現sleep和wait的區別:
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
// 日志行號記錄
private AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
// 開啟兩個線程去執行test方法
new Thread(main::test).start();
new Thread(main::test).start();
}
private synchronized void test() {
try {
log("進入了同步方法,並開始睡覺,1s");
// sleep不會釋放鎖,因此其他線程不能進入這個方法
Thread.sleep(1000);
log("睡好了,但沒事做,有事叫我,等待2s");
//阻塞在此,並且釋放鎖,其它線程可以進入這個方法
//當其它線程調用此對象的notify或者notifyAll時才有機會停止阻塞
//就算沒有人notify,如果超時了也會停止阻塞
wait(2000);
log("我要走了,但我要再睡一覺,10s");
//這里睡的時間很長,因為沒有釋放鎖,其它線程就算wait超時了也無法繼續執行
Thread.sleep(10000);
log("走了");
notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印日志
private void log(String s) {
System.out.println(count.incrementAndGet() + " "
+ new Date().toString().split(" ")[3]
+ "\t" + Thread.currentThread().getName() + " " + s);
}
}
/* 輸出:
1 00:13:23 Thread-0 進入了同步方法,並開始睡覺,1s
2 00:13:24 Thread-0 睡好了,但沒事做,有事叫我,等待2s
3 00:13:24 Thread-1 進入了同步方法,並開始睡覺,1s
4 00:13:25 Thread-1 睡好了,但沒事做,有事叫我,等待2s
5 00:13:26 Thread-0 我要走了,但我要再睡一覺,10s
6 00:13:36 Thread-0 走了
7 00:13:36 Thread-1 我要走了,但我要再睡一覺,10s
8 00:13:46 Thread-1 走了
*/
對輸出做個簡單解釋(已經看懂代碼的童鞋可以跳過):
1 00:13:23 Thread-0 進入了同步方法,並開始睡覺,1s // Thread-0首先進入同步方法,Thread-1只能門外候着
2 00:13:24 Thread-0 睡好了,但沒事做,有事叫我,等待2s // Thread-0 sleep 1秒這段時間,Thread-1沒進來,證明sleep沒有釋放鎖
3 00:13:24 Thread-1 進入了同步方法,並開始睡覺,1s // Thread-0開始wait后Thread-1馬上就進來了,證明wait釋放了鎖
4 00:13:25 Thread-1 睡好了,但沒事做,有事叫我,等待2s // Thread-1也打算wait 2秒(2秒后真的能醒來嗎?)
5 00:13:26 Thread-0 我要走了,但我要再睡一覺,10s // Thread-0已經wait超時醒來了,這次准備sleep 10s
6 00:13:36 Thread-0 走了 // 10s過去了Thread-0都sleep結束了,那個說要wait 2s的Thread-1還沒動靜,證明超時也沒用,還得搶到鎖
7 00:13:36 Thread-1 我要走了,但我要再睡一覺,10s // Thread-0退出同步代碼后,Thread-1才終於得到了鎖,能行動了
8 00:13:46 Thread-1 走了
notify和notifyAll
同樣是喚醒等待的線程,同樣最多只有一個線程能獲得鎖,同樣不能控制哪個線程獲得鎖。
區別在於:
- notify:喚醒一個線程,其他線程依然處於wait的等待喚醒狀態,如果被喚醒的線程結束時沒調用notify,其他線程就永遠沒人去喚醒,只能等待超時,或者被中斷
- notifyAll:所有線程退出wait的狀態,開始競爭鎖,但只有一個線程能搶到,這個線程執行完后,其他線程又會有一個幸運兒脫穎而出得到鎖
如果覺得解釋的不夠明白,代碼來一波:
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
private AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
// 開啟兩個線程去執行test方法
for (int i = 0; i < 10; i++) {
new Thread(main::testWait).start();
}
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
main.testNotify();
}
}
private synchronized void testWait() {
try {
log("進入了同步方法,開始wait");
wait();
log("wait結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void testNotify() {
notify();
}
private void log(String s) {
System.out.println(count.incrementAndGet() + " "
+ new Date().toString().split(" ")[3]
+ "\t" + Thread.currentThread().getName() + " " + s);
}
}
/* 輸出:
1 00:59:32 Thread-0 進入了同步方法,開始wait
2 00:59:32 Thread-9 進入了同步方法,開始wait
3 00:59:32 Thread-8 進入了同步方法,開始wait
4 00:59:32 Thread-7 進入了同步方法,開始wait
5 00:59:32 Thread-6 進入了同步方法,開始wait
6 00:59:32 Thread-5 進入了同步方法,開始wait
7 00:59:32 Thread-4 進入了同步方法,開始wait
8 00:59:32 Thread-3 進入了同步方法,開始wait
9 00:59:32 Thread-2 進入了同步方法,開始wait
10 00:59:32 Thread-1 進入了同步方法,開始wait
11 00:59:33 Thread-0 wait結束
12 00:59:33 Thread-6 wait結束
13 00:59:33 Thread-7 wait結束
14 00:59:33 Thread-8 wait結束
15 00:59:33 Thread-9 wait結束
*/
例子中有10個線程在wait,但notify了5次,然后其它線程一直阻塞,這也就說明使用notify時如果不能准確控制和wait的線程數對應,可能會導致某些線程永遠阻塞。
使用notifyAll喚醒所有等待的線程:
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
private AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
// 開啟兩個線程去執行test方法
for (int i = 0; i < 5; i++) {
new Thread(main::testWait).start();
}
Thread.sleep(1000);
main.testNotifyAll();
}
private synchronized void testWait() {
try {
log("進入了同步方法,開始wait");
wait();
log("wait結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void testNotifyAll() {
notifyAll();
}
private void log(String s) {
System.out.println(count.incrementAndGet() + " "
+ new Date().toString().split(" ")[3]
+ "\t" + Thread.currentThread().getName() + " " + s);
}
}
/* 輸出:
1 01:03:24 Thread-0 進入了同步方法,開始wait
2 01:03:24 Thread-4 進入了同步方法,開始wait
3 01:03:24 Thread-3 進入了同步方法,開始wait
4 01:03:24 Thread-2 進入了同步方法,開始wait
5 01:03:24 Thread-1 進入了同步方法,開始wait
6 01:03:25 Thread-1 wait結束
7 01:03:25 Thread-2 wait結束
8 01:03:25 Thread-3 wait結束
9 01:03:25 Thread-4 wait結束
10 01:03:25 Thread-0 wait結束
*/
只需要調用一次notifyAll,所有的等待線程都被喚醒,並且去競爭鎖,然后依次(無序)獲取鎖完成了后續任務。
為什么wait要放到循環中使用
一些源碼中出現wait時,往往都是伴隨着一個循環語句出現的,比如:
private synchronized void f() throws InterruptedException {
while (!isOk()) {
wait();
}
System.out.println("I'm ok");
}
既然wait會被阻塞直到被喚醒,那么用if+wait不就可以了嗎?其他線程發現條件達到時notify一下不就行了?
理想情況確實如此,但實際開發中我們往往不能保證這個線程被notify時條件已經滿足了,因為很可能有某個無關(和這個條件的邏輯無關)的線程因為需要線程調度而調用了notify或者notifyAll。此時如果樣例中位置等待的線程不巧被喚醒,它就會繼續往下執行,但因為用的if,這次被喚醒就不會再判斷條件是否滿足,最終程序按照我們不期望的方式執行下去。