博客發(fā)表于:Ghamster Blog
轉(zhuǎn)載請(qǐng)注明出處
概述
Java中可使用wait和notify(或notifyAll)方法同步對(duì)臨界資源的訪問
這些方法在Object類中定義,因此可以在任何對(duì)象上調(diào)用
在對(duì)象上調(diào)用wait方法使線程阻塞,在對(duì)象上調(diào)用notify或notifyAll會(huì)喚醒之前在該對(duì)象上調(diào)用wait阻塞的線程
調(diào)用這些方法之前需要線程已經(jīng)獲取對(duì)象的鎖(This method should only be called by a thread that is the owner of this object's monitor),否則會(huì)拋出java.lang.IllegalMonitorStateException。因此只能在同步方法或同步代碼塊中使用
wait
- 阻塞當(dāng)前線程,釋放鎖
-
wait()或wait(0),為無限期阻塞(兩者完全相同);wait(long timeoutMillis)或wait(long timeoutMillis, int nanos),若在參數(shù)指定時(shí)間內(nèi)沒有被喚醒或打斷,自動(dòng)恢復(fù)執(zhí)行 - 可以被
notify或notifyAll喚醒 - 可以被
interrupt方法打斷,拋出InterruptedException
notify & notifyAll
-
notify: 喚醒一個(gè)在該對(duì)象上調(diào)用wait方法阻塞的線程 -
notifyAll: 喚醒所有在該對(duì)象上調(diào)用wait方法阻塞的線程
notify與notifyAll測試
notify相對(duì)于notifyAll方法是一種性能優(yōu)化,因?yàn)?code>notify只會(huì)喚醒一個(gè)線程,但notifyAll會(huì)喚醒所有等待的線程,使他們競爭cpu;但同時(shí),使用notify你必須確定被喚醒的是合適的線程
下面的測試代碼展示了“必須喚醒合適線程的問題”
-
Critical類只包含一個(gè)Color類的對(duì)象,通過對(duì)象初始化語句賦值為Color.B -
ColorModifier類實(shí)現(xiàn)了Runnable接口,包含三個(gè)域:critical、target和to,操作critical的color對(duì)象,當(dāng)與目標(biāo)顏色target相符時(shí),將顏色修改為to指定的值。 -
main方法中,依次創(chuàng)建三個(gè)ColorModifier類的實(shí)例,分別為R->G,G->B,B->R,交給ExectorService執(zhí)行,30s后關(guān)閉ExectorService,三個(gè)線程收到InterruptedException退出
使用notifyAll的測試代碼如下:
package main.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TestNotify {
enum Color {R, G, B}
private static class Critical {
public Color color = Color.R;
}
private static class ColorModifier implements Runnable {
private Critical critical;
private Color target;
private Color to;
public ColorModifier(Critical critical, Color target, Color to) {
this.critical = critical;
this.target = target;
this.to = to;
}
@Override
public void run() {
System.out.printf("-> Thread start: Modifier %s to %s\n", target, to);
try {
while (!Thread.interrupted()) {
synchronized (critical) {
while (critical.color != target) {
System.out.printf(" - Wait: Modifier %s -> %s, Current color: %s\n", target, to, critical.color);
critical.wait();
System.out.printf(" + Resume from wait: Modifier %s -> %s, Current color: %s\n", target, to, critical.color);
}
//change critical.color and notify others
critical.color = to;
System.out.printf("\n>>> Color changed: %s to %s!\n", target, to);
TimeUnit.SECONDS.sleep(1);
critical.notifyAll();
}
}
} catch (InterruptedException e) {
System.out.printf("Thread Modifier %s -> %s exit!\n", target, to);
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
Critical c = new Critical();
exec.execute(new ColorModifier(c, Color.R, Color.G));
exec.execute(new ColorModifier(c, Color.G, Color.B));
exec.execute(new ColorModifier(c, Color.B, Color.R));
TimeUnit.SECONDS.sleep(30);
exec.shutdownNow();
}
}
}
輸出如下:
-> Thread start: Modifier R to G
>>> Color changed: R to G!
-> Thread start: Modifier B to R
-> Thread start: Modifier G to B
- Wait: Modifier R -> G, Current color: G
>>> Color changed: G to B!
- Wait: Modifier G -> B, Current color: B
>>> Color changed: B to R!
- Wait: Modifier B -> R, Current color: R
+ Resume from wait: Modifier G -> B, Current color: R
- Wait: Modifier G -> B, Current color: R
+ Resume from wait: Modifier R -> G, Current color: R
>>> Color changed: R to G!
- Wait: Modifier R -> G, Current color: G
+ Resume from wait: Modifier B -> R, Current color: G
- Wait: Modifier B -> R, Current color: G
+ Resume from wait: Modifier G -> B, Current color: G
>>> Color changed: G to B!
- Wait: Modifier G -> B, Current color: B
+ Resume from wait: Modifier R -> G, Current color: B
- Wait: Modifier R -> G, Current color: B
+ Resume from wait: Modifier B -> R, Current color: B
>>> Color changed: B to R!
... ...
Thread Modifier B -> R exit!
Thread Modifier R -> G exit!
Thread Modifier G -> B exit!
Process finished with exit code 0
任意時(shí)刻,系統(tǒng)中有三個(gè)ColorModifier的線程(更嚴(yán)謹(jǐn)?shù)谋硎鍪牵簍arget為ColorModifer對(duì)象的線程)RtoG、GtoB和BtoR,假設(shè)RtoG修改顏色后(console第17行),調(diào)用notifyAll方法,使GtoB、BtoR線程被喚醒,三個(gè)線程均可開始(繼續(xù))執(zhí)行。當(dāng)前顏色為Color.G,執(zhí)行至代碼32行,RtoG和BtoR調(diào)用wait阻塞,GtoB修改顏色并調(diào)用notifyAll方法,如此往復(fù)
測試notify方法時(shí),將第40行代碼修改為critical.notify();,輸出如下:
-> Thread start: Modifier B to R
-> Thread start: Modifier R to G
-> Thread start: Modifier G to B
- Wait: Modifier B -> R, Current color: R
- Wait: Modifier G -> B, Current color: R
>>> Color changed: R to G!
- Wait: Modifier R -> G, Current color: G
+ Resume from wait: Modifier B -> R, Current color: G
- Wait: Modifier B -> R, Current color: G
Thread Modifier B -> R exit!
Thread Modifier R -> G exit!
Thread Modifier G -> B exit!
Process finished with exit code 0
每次運(yùn)行測試得到的輸出各不相同,但幾乎所有的測試都會(huì)導(dǎo)致死鎖,直到時(shí)間耗盡,調(diào)用ExectorService.shutdownNow()結(jié)束程序。以本次運(yùn)行結(jié)果為例,RtoG、GtoB和BtoR依次啟動(dòng),Critical對(duì)象初始顏色為Color.R。執(zhí)行至代碼32行,BtoR和GtoB調(diào)用wait阻塞(對(duì)應(yīng)console第4-5行);RtoG將顏色修改為Color.G,調(diào)用notify方法,BtoR被喚醒;RtoG繼續(xù)執(zhí)行,經(jīng)過代碼32行判斷后調(diào)用wait阻塞;BtoR被喚醒后,經(jīng)過32行同樣調(diào)用wait阻塞 -- 至此三個(gè)線程全部阻塞,程序陷入死鎖。
對(duì)于本程序而言,“合適的線程”是指:BtoR的notify必須喚醒RtoG,RtoG的notify必須喚醒GtoB,GtoB的notify必須喚醒BtoR
one more thing
如果對(duì)測試代碼稍作修改會(huì)發(fā)生有趣的事情:
- 將
Critical對(duì)象的color屬性初始值設(shè)為Color.B(12行) - 在
main方法的每個(gè)exec.execute()方法后插入TimeUnit.SECONDS.sleep(1);,插入后代碼如下:
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
Critical c = new Critical();
exec.execute(new ColorModifier(c, Color.R, Color.G));
TimeUnit.SECONDS.sleep(1);
exec.execute(new ColorModifier(c, Color.G, Color.B));
TimeUnit.SECONDS.sleep(1);
exec.execute(new ColorModifier(c, Color.B, Color.R));
TimeUnit.SECONDS.sleep(30);
exec.shutdownNow();
}
此時(shí)會(huì)得到如下輸出:
>>> Color changed: B to R!
- Wait: Modifier B -> R, Current color: R
+ Resume from wait: Modifier R -> G, Current color: R
>>> Color changed: R to G!
- Wait: Modifier R -> G, Current color: G
+ Resume from wait: Modifier G -> B, Current color: G
>>> Color changed: G to B!
- Wait: Modifier G -> B, Current color: B
+ Resume from wait: Modifier B -> R, Current color: B
程序并未出現(xiàn)死鎖!似乎BtoR的notify總會(huì)喚醒RtoG,RtoG會(huì)喚醒GtoB,GtoB會(huì)喚醒BtoR
換言之,notify被調(diào)用時(shí),喚醒的線程不是隨機(jī)的,而是所有阻塞的線程中,最早調(diào)用wait的那個(gè)
測試
測試環(huán)境:window x64,jdk11
- 內(nèi)部類
WaitAndNotify實(shí)現(xiàn)了Runnable接口,構(gòu)造方法需要傳入一個(gè)Object對(duì)象(o) - 在
run方法中,首先調(diào)用o.wait()阻塞,被喚醒后調(diào)用o.notify() -
main方法依次產(chǎn)生THREAD_NUMBERS個(gè)使用WaitAndNotify對(duì)象創(chuàng)建的線程,并交由ExecutorService執(zhí)行;在main方法中調(diào)用notify,引發(fā)鏈?zhǔn)椒磻?yīng),使所有線程依次執(zhí)行 - 使用
CountDownLatch計(jì)數(shù),所有線程完成后,關(guān)閉ExecutorService退出程序
測試代碼如下:
package main.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TestSynchronizedLockOrder {
private static final int THREAD_NUMBERS = 5;
private static class WaitAndNotify implements Runnable {
private static int count = 0;
private int id = count++;
private CountDownLatch countDownLatch;
private Object o;
public WaitAndNotify(Object o, CountDownLatch c) {
this.o = o;
this.countDownLatch = c;
}
@Override
public void run() {
synchronized (o) {
try {
System.out.println("WAN id=" + id + " call wait");
o.wait();
TimeUnit.SECONDS.sleep(1);
System.out.println("WAN id=" + id + " running");
o.notify();
System.out.println("WAN id=" + id + " call notify");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUMBERS);
ExecutorService e = Executors.newCachedThreadPool();
for (int i = 0; i < THREAD_NUMBERS; i++) {
e.execute(new WaitAndNotify(o, countDownLatch));
TimeUnit.SECONDS.sleep(1);
}
System.out.println("===================\nAll thread started!\n===================");
synchronized (o) {
o.notify();
}
countDownLatch.await();
e.shutdownNow();
}
}
程序輸出如下:
WAN id=0 call wait
WAN id=1 call wait
WAN id=2 call wait
WAN id=3 call wait
WAN id=4 call wait
===================
All thread started!
===================
WAN id=0 running
WAN id=0 call notify
WAN id=1 running
WAN id=1 call notify
WAN id=2 running
WAN id=2 call notify
WAN id=3 running
WAN id=3 call notify
WAN id=4 running
WAN id=4 call notify
Process finished with exit code 0
結(jié)論
顯然,在本平臺(tái)上調(diào)用notify方法時(shí),被喚醒的永遠(yuǎn)是最早調(diào)用wait方法阻塞的線程,但這個(gè)結(jié)論是否具有普遍性?
jdk文檔對(duì)于notify的描述如下:
Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation...
The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object...
參考jdk文檔的內(nèi)容,總結(jié)來說有兩點(diǎn):
- 調(diào)用
notify方法會(huì)喚醒一個(gè)阻塞的線程,且這個(gè)線程是隨機(jī)的,且不同平臺(tái)可以有不同實(shí)現(xiàn) - 被喚醒的線程需要競爭臨界資源,相比于其他線程不具有更高或更低的優(yōu)先級(jí)
因此,這種測試結(jié)果只能算平臺(tái)的特例……
《全劇終》