Java中的Object類是所有類的父類,鑒于繼承機(jī)制卖词,Java把所有的類都需的方法放在了Object類里面吭练,其中就包含要說的通知與等待饱溢。
1.wait()方法
當(dāng)一個(gè)線程調(diào)用一個(gè)共享變量的wait()方法時(shí),該調(diào)用線程會(huì)被阻塞掛起,直到發(fā)生下面幾件事情之一才返回是嗜。
? 1.其他線程調(diào)用了該共享對(duì)象的 notify()或者 notifyAll() 方法痹筛。
? 2.其他線程調(diào)用了該線程的 interrupt() 方法可免,該線程拋出 InterruptedException 異常返回
另外需要注意的是耕漱,如果調(diào)用 wait() 方法的線程沒有事先獲取該對(duì)象的監(jiān)視器鎖,則調(diào)用wait()方法時(shí)調(diào)用線程會(huì)拋出 IllegalMonitorStateException異常尽爆。
那么一個(gè)線程如何才能獲取一個(gè)共享變量的監(jiān)視器鎖呢?
? 1.執(zhí)行synchronized同步代碼塊時(shí)使用該共享變量作為參數(shù)怎顾。
synchronized(共享變量){
// doSomething
}
? 2.調(diào)用該共享變量的方法,并且該方法使用了 synchronized 修飾漱贱。
synchronized void method(int a, int b){
// doSomething
}
另外需要注意的時(shí)槐雾,一個(gè)線程可以從掛起狀態(tài)變?yōu)榭梢赃\(yùn)行的狀態(tài)(也就是被喚醒),即使該線程沒有被其他線程調(diào)用notify(), notifyAll() 方法進(jìn)行通知幅狮,或者被中斷募强,或者等待超時(shí)。也就是所謂的虛假喚醒崇摄。
雖然虛假喚醒在應(yīng)用實(shí)踐中很少發(fā)生擎值,但要防患于未然,做法就是不停地去測試該線程被喚醒狀態(tài)的條件是否滿足配猫,不滿足則繼續(xù)等待幅恋,也就是說在一個(gè)循環(huán)中調(diào)用wait()方法進(jìn)行防范。退出循環(huán)的條件就是滿足了喚醒該線程的條件泵肄。
synchronized (obj) {
while(條件不滿足){
obj.wait();
}
}
如上代碼呢也是經(jīng)典的調(diào)用共享變量wait()方法的實(shí)例,首先通過同步塊獲取obj上面的監(jiān)視器鎖淑翼,然后再while 循環(huán)內(nèi)調(diào)用obj的wait()方法腐巢。
下面從一個(gè)簡單的生產(chǎn)者和消費(fèi)者例子來加深下理解。如下面代碼所示玄括,其中queue為共享變量冯丙,生產(chǎn)者線程在調(diào)用queue的wait()方法前,使用synchronized關(guān)鍵字拿到了該共享變量queue的監(jiān)視器鎖,所以調(diào)用wait()方法不會(huì)拋出 IllegalMonitorStateException 異常胃惜。如果當(dāng)前隊(duì)列沒有空閑容量則會(huì)調(diào)用wait()方法掛起當(dāng)前線程泞莉,這里使用循環(huán)是為了避免上面說的虛假喚醒。假如當(dāng)前線程被虛假喚醒了船殉,但是隊(duì)列還是沒有空余的容量鲫趁,那么當(dāng)前線程還是會(huì)調(diào)用wait()方法把自己掛起。
public class ObjectMethodTest {
Queue<String> queue = new LinkedList<String>();
int MAX_SIZE = 1; // 假設(shè)隊(duì)列長度只有1 利虫, 只能存放一條數(shù)據(jù)
public void produce(){
synchronized (queue){
// 隊(duì)列滿則等待隊(duì)列空間
while (queue.size() == MAX_SIZE) {
// 掛起當(dāng)前線程挨厚,并釋放通過同步塊獲取的queue上的鎖,讓消費(fèi)者線程可以獲取該鎖糠惫,然后獲取隊(duì)列里面的元素疫剃。
try {
queue.wait();
System.out.println("-----等待消費(fèi)----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add("hahaha");
queue.notifyAll();
}
}
public void consume(){
synchronized (queue) {
while (queue.size() == 0) {
// 掛起當(dāng)前線程,并釋放通過同步塊獲取的queue上的鎖硼讽,讓消費(fèi)者線程可以獲取該鎖巢价,然后獲取隊(duì)列里面的元素。
try {
queue.wait();
System.out.println("-----等待生產(chǎn)-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消費(fèi)元素, 并通知喚醒生產(chǎn)者
System.out.println("消費(fèi)成功:" + queue.poll());
queue.notifyAll();
}
}
public static void main(String[] args) {
ObjectMethodTest objectMethodTest = new ObjectMethodTest();
// 10 個(gè)生產(chǎn)線程
for (int i = 0; i < 10; i++) {
new Thread(objectMethodTest::produce).start();
}
// 10 個(gè)消費(fèi)線程
for (int i = 0; i < 10; i++) {
new Thread(objectMethodTest::consume).start();
}
}
}
在如上代碼中固阁,假如生產(chǎn)者線程A首先通過synchronized獲取到了queue上的鎖蹄溉,那么后續(xù)所有企圖生產(chǎn)的線程和消費(fèi)的線程 都將會(huì)在獲取該監(jiān)視器鎖的地方被阻塞掛起。線程A獲取鎖后發(fā)現(xiàn)隊(duì)列已滿會(huì)調(diào)用wait()方法阻塞掛起自己您炉,然后就會(huì)釋放掉獲取到的queue上的鎖柒爵,防止發(fā)生死鎖。
另外需要注意的是赚爵,當(dāng)前線程調(diào)用共享變量的wait()方法后指揮釋放當(dāng)前共享變量上的鎖棉胀,如果當(dāng)前線程還持有其他共享變量的鎖,則這些鎖是不會(huì)被釋放的冀膝,接下來看例子唁奢。
public class WaitTest {
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程
Thread threadA = new Thread(() -> {
try {
synchronized (resourceA) {
System.out.println("threadA 獲取到resourceA的鎖");
synchronized (resourceB){
System.out.println("threadA 獲取到 resourceB的鎖");
System.out.println("threadA 釋放掉 resourceA的鎖");
resourceA.wait();
}
}
}catch (Exception ex) {
ex.printStackTrace();
}
});
// 創(chuàng)建線程
Thread threadB = new Thread(() -> {
try {
Thread.sleep(1000);
synchronized (resourceA) {
System.out.println("threadB 獲取到resourceA的鎖");
System.out.println("threadB 嘗試獲取resourceB的鎖*****");
synchronized (resourceB){
System.out.println("threadB 獲取到 resourceB的鎖");
System.out.println("threadB 釋放掉 resourceA的鎖");
resourceA.wait();
}
}
}catch (Exception ex) {
ex.printStackTrace();
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
如上代碼在main方法里 啟動(dòng)了 A,B兩個(gè)線程,為了讓A先獲取到鎖窝剖,這里讓線程B休眠了1s麻掸,線程A先后獲取到了共享變量resourceA和resourceB上的鎖,然后調(diào)用了resourceA的wait()方法阻塞掛起自己赐纱,阻塞自己后線程A釋放掉了獲取到的resourceA上的鎖脊奋。
線程B休眠后結(jié)束后會(huì)先嘗試獲取resourceA上的鎖,如果當(dāng)前線程A里邊還沒有調(diào)用resourceA的wait()方法阻塞掛起釋放掉該鎖疙描,那么線程B就會(huì)被阻塞诚隙,如果線程A釋放了resourceA的鎖后,線程B就會(huì)獲取到resourceA上的鎖起胰,然后嘗試獲取resourceB上的鎖久又。由于線程A中沒有釋放鎖,所以導(dǎo)致線程B嘗試獲取resourceB上的鎖時(shí)會(huì)被阻塞。
以上就證明了當(dāng)前線程調(diào)用共享變量對(duì)象的wait()方法時(shí)地消,當(dāng)前線程只會(huì)釋放當(dāng)前共享對(duì)象的鎖炉峰,當(dāng)前線程持有其他共享對(duì)象的監(jiān)視器鎖并不會(huì)被釋放。
這里再舉個(gè)例子說明當(dāng)一個(gè)線程調(diào)用共享對(duì)象的wait()方法被阻塞掛起后脉执,如果其他線程中斷了該線程疼阔,則該線程會(huì)拋出InterruptedException異常返回。
public class WaitNotifyInterrupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread testThread = new Thread(() -> {
try {
System.out.println("----begin----");
// 阻塞當(dāng)前線程
synchronized (obj) {
obj.wait();
}
System.out.println("---end---");
} catch (Exception ex) {
ex.printStackTrace();
}
});
testThread.start();
Thread.sleep(1000);
System.out.println("開始阻斷 testThread");
testThread.interrupt();
System.out.println("阻塞 testThread 完畢");
}
}
如上代碼适瓦,testThread調(diào)用了共享變量obj的wait()方法后阻塞掛起了自己竿开,然后主線程休眠1s后中斷了testThread線程,中斷后testThread再obj.wait()處拋出了java.lang.InterruptedException 異常而返回并終止玻熙。
2.wait(long timeout)方法
該方法相比于wait()方法多了一個(gè)超時(shí)參數(shù)否彩,它的不同之處在于,如果一個(gè)線程調(diào)用共享變量的該方法掛起后嗦随,沒有再指定的timeout ms時(shí)間內(nèi)被其他線程調(diào)用該共享變量的notify()或者notifyAll()方法喚醒列荔,那么該函數(shù)還是因?yàn)槌瑫r(shí)而返回。如果將timeoout設(shè)置為0那么則和wait()方法效果一樣枚尼,因?yàn)閣ait()方法內(nèi)部就是調(diào)用了wait(0)贴浙,需要注意的是,如果在調(diào)用該方法時(shí)署恍,傳遞了一個(gè)負(fù)的timeout則會(huì)拋出IllegalArgumentException異常崎溃。
3.wait(long timeout, int nanos)方法
在內(nèi)部調(diào)用的是wait(long timeout)函數(shù),如下代碼只用nanos>0時(shí)才使timeout參數(shù)遞增1盯质。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
4.notify()方法
一個(gè)線程調(diào)用共享對(duì)象的notify方法后袁串,會(huì)喚醒一個(gè)在該共享變量上調(diào)用wait系列方法后被阻塞掛起的線程。一個(gè)共享變量上可能會(huì)有多個(gè)線程在等待呼巷,具體喚醒哪個(gè)等待的線程是隨機(jī)的囱修。
此外,被喚醒的線程不能馬上從wait方法返回并繼續(xù)執(zhí)行王悍,它必須在獲取了共享對(duì)象的監(jiān)視器鎖后才可以返回破镰,也就是喚醒它的線程釋放了共享變量上的監(jiān)視器鎖后,被喚醒的線程也不一定會(huì)獲取到共享變量的監(jiān)視器鎖压储,這是因?yàn)樵摼€程還需要和其他線程一起競爭該鎖鲜漩,只有該線程競爭到了共享變量的監(jiān)視器鎖后才能繼續(xù)執(zhí)行。
類似wait()系列方法渠脉,只有當(dāng)前線程獲取到了共享變量的監(jiān)視器鎖后宇整,才可以調(diào)用共享變量的notify()方法,否則會(huì)拋出 IllegalMonitorStateException異常芋膘。
5.notifyAll()方法
不同于在共享變量上調(diào)用notify(),會(huì)喚醒被阻塞到該共享變量上的一個(gè)線程,notifyAll()方法則會(huì)喚醒所有在該共享變量由于調(diào)用wait()系列方法而被掛起的線程为朋。
下面舉個(gè)例子來說明notify()和notifyAll()方法具體含義以及一些需要注意的地方臂拓。
public class NotifyTest {
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程
Thread threadA = new Thread(() -> {
// 獲取resourceA的共享資源鎖
synchronized (resourceA) {
System.out.println("threadA 獲取到 resourceA 的鎖");
try {
System.out.println("threadA 開始調(diào)用resourceA的wait()方法進(jìn)行阻塞掛起");
resourceA.wait();
System.out.println("threadA 結(jié)束等待");
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
// 創(chuàng)建線程
Thread threadB = new Thread(() -> {
synchronized (resourceA) {
System.out.println("threadB 獲取到 resourceA 的鎖");
try {
System.out.println("threadB 開始調(diào)用resourceA的wait()方法進(jìn)行阻塞掛起");
resourceA.wait();
System.out.println("threadB 結(jié)束等待");
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
// 創(chuàng)建線程
Thread threadC = new Thread(() -> {
synchronized (resourceA) {
System.out.println("threadC 開始調(diào)用 resourceA的notify()方法");
resourceA.notify();
}
});
// 啟動(dòng)線程
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
// 等待線程執(zhí)行結(jié)束
threadA.join();
threadB.join();
threadC.join();
System.out.println("end---------------------");
}
}
如上代碼開啟了三個(gè)線程,其中A,B線程分別調(diào)用了resourceA的wait()方法习寸,線程C在主線程休眠1s后調(diào)用了notify()方法胶惰。主線程休息1s是為了保證讓線程A,B全部執(zhí)行完wait()方法后再調(diào)用線程C的notify()方法。
這個(gè)例子試圖再線程A和線程B都因調(diào)用共享資源resourceA的wait()方法而被阻塞后霞溪,讓線程C調(diào)用resourceA的notify()方法孵滞,從而喚醒線程A,B。但是從執(zhí)行結(jié)果來看鸯匹,只有一個(gè)線程A被喚醒坊饶,線程B依然在阻塞掛起狀態(tài)。
從輸出結(jié)果可知線程調(diào)度器這次先調(diào)度了線程A占用Cpu來運(yùn)行殴蓬,線程A先獲取到resourceA的資源所匿级,然后調(diào)用wait()方法阻塞掛起,釋放鎖染厅,而后線程B獲取到資源鎖痘绎,調(diào)用resourceA的wait()阻塞掛起。然后線程C調(diào)用notify()方法肖粮,嘗試喚醒線程孤页,這回激活resourceA的阻塞集合里面的一個(gè)線程,這里激活了線程A涩馆,所以線程A方法執(zhí)行完畢并返回了行施。線程B則繼續(xù)在阻塞等待中。如果把notify()方法換成notifyAll()結(jié)果會(huì)這樣凌净。
換成notifyAll()方法后悲龟,可以看到都得到了喚醒。因?yàn)樯线呉舱f過了notifyAll()方法會(huì)喚醒共享變量內(nèi)所有的等待線程冰寻。這里就是喚醒了resourceA的等待集合里所有線程须教。只是線程B先搶到了resourceA上的鎖,然后返回斩芭。然后線程A搶到也進(jìn)行了返回轻腺。
嘗試把主線程里面的休眠1s去掉,看一下執(zhí)行結(jié)果划乖。
線程B沒有正常被喚醒贬养。
這是因?yàn)榫€程C可能比線程B先執(zhí)行了。如果調(diào)用notifyAll()方法后一個(gè)線程調(diào)用了該共享變量的wait()方法而被放到阻塞集合琴庵,則該線程不會(huì)被喚醒的误算,指揮喚醒執(zhí)行notifyAll()方法前阻塞集合里的所有線程仰美。
對(duì)wait(),notify(),notifyAll()的說明結(jié)束了