- Java并發編程之美
- 翟陸續 薛賓田
- 3779字
- 2019-07-25 11:53:59
1.3 線程通知與等待
Java中的Object類是所有類的父類,鑒于繼承機制,Java把所有類都需要的方法放到了Object類里面,其中就包含本節要講的通知與等待系列函數。
1.wait()函數
當一個線程調用一個共享變量的wait()方法時,該調用線程會被阻塞掛起,直到發生下面幾件事情之一才返回:(1)其他線程調用了該共享對象的notify()或者notifyAll()方法;(2)其他線程調用了該線程的interrupt()方法,該線程拋出InterruptedException異常返回。
另外需要注意的是,如果調用wait()方法的線程沒有事先獲取該對象的監視器鎖,則調用wait()方法時調用線程會拋出IllegalMonitorStateException異常。
那么一個線程如何才能獲取一個共享變量的監視器鎖呢?
(1)執行synchronized同步代碼塊時,使用該共享變量作為參數。
synchronized(共享變量){ //doSomething }
(2)調用該共享變量的方法,并且該方法使用了synchronized修飾。
synchronized void add(int a, int b){ //doSomething }
另外需要注意的是,一個線程可以從掛起狀態變為可以運行狀態(也就是被喚醒),即使該線程沒有被其他線程調用notify()、notifyAll()方法進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒。
雖然虛假喚醒在應用實踐中很少發生,但要防患于未然,做法就是不停地去測試該線程被喚醒的條件是否滿足,不滿足則繼續等待,也就是說在一個循環中調用wait()方法進行防范。退出循環的條件是滿足了喚醒該線程的條件。
synchronized (obj) { while (條件不滿足){ obj.wait(); } }
如上代碼是經典的調用共享變量wait()方法的實例,首先通過同步塊獲取obj上面的監視器鎖,然后在while循環內調用obj的wait()方法。
下面從一個簡單的生產者和消費者例子來加深理解。如下面代碼所示,其中queue為共享變量,生產者線程在調用queue的wait()方法前,使用synchronized關鍵字拿到了該共享變量queue的監視器鎖,所以調用wait()方法才不會拋出IllegalMonitorStateException異常。如果當前隊列沒有空閑容量則會調用queued的wait()方法掛起當前線程,這里使用循環就是為了避免上面說的虛假喚醒問題。假如當前線程被虛假喚醒了,但是隊列還是沒有空余容量,那么當前線程還是會調用wait()方法把自己掛起。
//生產線程 synchronized (queue) { //消費隊列滿,則等待隊列空閑 while (queue.size() == MAX_SIZE) { try { //掛起當前線程,并釋放通過同步塊獲取的queue上的鎖,讓消費者線程可以獲取該鎖,然后 獲取隊列里面的元素 queue.wait(); } catch (Exception ex) { ex.printStackTrace(); } } //空閑則生成元素,并通知消費者線程 queue.add(ele); queue.notifyAll(); } } //消費者線程 synchronized (queue) { //消費隊列為空 while (queue.size() == 0) { try //掛起當前線程,并釋放通過同步塊獲取的queue上的鎖,讓生產者線程可以獲取該鎖,將生 產元素放入隊列 queue.wait();
} catch (Exception ex) { ex.printStackTrace(); } } //消費元素,并通知喚醒生產者線程 queue.take(); queue.notifyAll(); } }
在如上代碼中假如生產者線程A首先通過synchronized獲取到了queue上的鎖,那么后續所有企圖生產元素的線程和消費線程將會在獲取該監視器鎖的地方被阻塞掛起。線程A獲取鎖后發現當前隊列已滿會調用queue.wait()方法阻塞自己,然后釋放獲取的queue上的鎖,這里考慮下為何要釋放該鎖?如果不釋放,由于其他生產者線程和所有消費者線程都已經被阻塞掛起,而線程A也被掛起,這就處于了死鎖狀態。這里線程A掛起自己后釋放共享變量上的鎖,就是為了打破死鎖必要條件之一的持有并等待原則。關于死鎖后面的章節會講。線程A釋放鎖后,其他生產者線程和所有消費者線程中會有一個線程獲取queue上的鎖進而進入同步塊,這就打破了死鎖狀態。
另外需要注意的是,當前線程調用共享變量的wait()方法后只會釋放當前共享變量上的鎖,如果當前線程還持有其他共享變量的鎖,則這些鎖是不會被釋放的。下面來看一個例子。
// 創建資源 private static volatile Object resourceA = new Object(); private static volatile Object resourceB = new Object(); public static void main(String[] args) throws InterruptedException { // 創建線程 Thread threadA = new Thread(new Runnable() { public void run() { try { // 獲取resourceA共享資源的監視器鎖 synchronized (resourceA) { System.out.println("threadA get resourceA lock");
// 獲取resourceB共享資源的監視器鎖 synchronized (resourceB) { System.out.println("threadA get resourceB lock"); // 線程A阻塞,并釋放獲取到的resourceA的鎖 System.out.println("threadA release resourceA lock"); resourceA.wait(); } } } catch (InterruptedException e) { e.printStackTrace(); } } }); // 創建線程 Thread threadB = new Thread(new Runnable() { public void run() { try { //休眠1s Thread.sleep(1000); // 獲取resourceA共享資源的監視器鎖 synchronized (resourceA) { System.out.println("threadB get resourceA lock"); System.out.println("threadB try get resourceB lock..."); // 獲取resourceB共享資源的監視器鎖 synchronized (resourceB) { System.out.println("threadB get resourceB lock"); // 線程B阻塞,并釋放獲取到的resourceA的鎖 System.out.println("threadB release resourceA lock"); resourceA.wait(); }
} } catch (InterruptedException e) { e.printStackTrace(); } } }); // 啟動線程 threadA.start(); threadB.start(); // 等待兩個線程結束 threadA.join(); threadB.join(); System.out.println("main over"); }
輸出結果如下:

如上代碼中,在main函數里面啟動了線程A和線程B,為了讓線程A先獲取到鎖,這里讓線程B先休眠了1s,線程A先后獲取到共享變量resourceA和共享變量resourceB上的鎖,然后調用了resourceA的wait()方法阻塞自己,阻塞自己后線程A釋放掉獲取的resourceA上的鎖。
線程B休眠結束后會首先嘗試獲取resourceA上的鎖,如果當時線程A還沒有調用wait()方法釋放該鎖,那么線程B會被阻塞,當線程A釋放了resourceA上的鎖后,線程B就會獲取到resourceA上的鎖,然后嘗試獲取resourceB上的鎖。由于線程A調用的是resourceA上的wait()方法,所以線程A掛起自己后并沒有釋放獲取到的resourceB上的鎖,所以線程B嘗試獲取resourceB上的鎖時會被阻塞。
這就證明了當線程調用共享對象的wait()方法時,當前線程只會釋放當前共享對象的鎖,當前線程持有的其他共享對象的監視器鎖并不會被釋放。
最后再舉一個例子進行說明。當一個線程調用共享對象的wait()方法被阻塞掛起后,如果其他線程中斷了該線程,則該線程會拋出InterruptedException異常并返回。
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//創建線程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println("---begin---");
//阻塞當前線程
synchronized (obj) {
obj.wait();
}
System.out.println("---end---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
}
}
輸出如下。

在如上代碼中,threadA調用共享對象obj的wait()方法后阻塞掛起了自己,然后主線程在休眠1s后中斷了threadA線程,中斷后threadA在obj.wait()處拋出java.lang. InterruptedException異常而返回并終止。
2.wait(long timeout)函數
該方法相比wait()方法多了一個超時參數,它的不同之處在于,如果一個線程調用共享對象的該方法掛起后,沒有在指定的timeout ms時間內被其他線程調用該共享變量的notify()或者notifyAll()方法喚醒,那么該函數還是會因為超時而返回。如果將timeout設置為0則和wait方法效果一樣,因為在wait方法內部就是調用了wait(0)。需要注意的是,如果在調用該函數時,傳遞了一個負的timeout則會拋出IllegalArgumentException異常。
3.wait(long timeout, int nanos) 函數
在其內部調用的是wait(long timeout)函數,如下代碼只有在nanos>0時才使參數timeout遞增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() 函數
一個線程調用共享對象的notify()方法后,會喚醒一個在該共享變量上調用wait系列方法后被掛起的線程。一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是隨機的。
此外,被喚醒的線程不能馬上從wait方法返回并繼續執行,它必須在獲取了共享對象的監視器鎖后才可以返回,也就是喚醒它的線程釋放了共享變量上的監視器鎖后,被喚醒的線程也不一定會獲取到共享對象的監視器鎖,這是因為該線程還需要和其他線程一起競爭該鎖,只有該線程競爭到了共享變量的監視器鎖后才可以繼續執行。
類似wait系列方法,只有當前線程獲取到了共享變量的監視器鎖后,才可以調用共享變量的notify()方法,否則會拋出IllegalMonitorStateException異常。
5.notifyAll() 函數
不同于在共享變量上調用notify()函數會喚醒被阻塞到該共享變量上的一個線程,notifyAll()方法則會喚醒所有在該共享變量上由于調用wait系列方法而被掛起的線程。
下面舉一個例子來說明notify()和notifyAll()方法的具體含義及一些需要注意的地方,代碼如下。
// 創建資源 private static volatile Object resourceA = new Object(); public static void main(String[] args) throws InterruptedException { // 創建線程 Thread threadA = new Thread(new Runnable() { public void run() { // 獲取resourceA共享資源的監視器鎖 synchronized (resourceA) { System.out.println("threadA get resourceA lock"); try { System.out.println("threadA begin wait"); resourceA.wait(); System.out.println("threadA end wait");
} catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }); // 創建線程 Thread threadB = new Thread(new Runnable() { public void run() { synchronized (resourceA) { System.out.println("threadB get resourceA lock"); try { System.out.println("threadB begin wait"); resourceA.wait(); System.out.println("threadB end wait"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }); // 創建線程 Thread threadC = new Thread(new Runnable() { public void run() { synchronized (resourceA) { System.out.println("threadC begin notify"); resourceA.notify(); } } }); // 啟動線程 threadA.start();
threadB.start(); Thread.sleep(1000); threadC.start(); // 等待線程結束 threadA.join(); threadB.join(); threadC.join(); System.out.println("main over"); }
輸出結果如下。

如上代碼開啟了三個線程,其中線程A和線程B分別調用了共享資源resourceA的wait()方法,線程C則調用了nofity()方法。這里啟動線程C前首先調用sleep方法讓主線程休眠1s,這樣做的目的是讓線程A和線程B全部執行到調用wait方法后再調用線程C的notify方法。這個例子試圖在線程A和線程B都因調用共享資源resourceA的wait()方法而被阻塞后,讓線程C再調用resourceA的notify()方法,從而喚醒線程A和線程B。但是從執行結果來看,只有一個線程A被喚醒,線程B沒有被喚醒:
從輸出結果可知線程調度器這次先調度了線程A占用CPU來運行,線程A首先獲取resourceA上面的鎖,然后調用resourceA的wait()方法掛起當前線程并釋放獲取到的鎖,然后線程B獲取到resourceA上的鎖并調用resourceA的wait()方法,此時線程B也被阻塞掛起并釋放了resourceA上的鎖,到這里線程A和線程B都被放到了resourceA的阻塞集合里面。線程C休眠結束后在共享資源resourceA上調用了notify()方法,這會激活resourceA的阻塞集合里面的一個線程,這里激活了線程A,所以線程A調用的wait()方法返回了,線程A執行完畢。而線程B還處于阻塞狀態。如果把線程C調用的notify()方法改為調用notifyAll()方法,則執行結果如下。

從輸入結果可知線程A和線程B被掛起后,線程C調用notifyAll()方法會喚醒resourceA的等待集合里面的所有線程,這里線程A和線程B都會被喚醒,只是線程B先獲取到resourceA上的鎖,然后從wait()方法返回。線程B執行完畢后,線程A又獲取了resourceA上的鎖,然后從wait()方法返回。線程A執行完畢后,主線程返回,然后打印輸出。
一個需要注意的地方是,在共享變量上調用notifyAll()方法只會喚醒調用這個方法前調用了wait系列函數而被放入共享變量等待集合里面的線程。如果調用notifyAll()方法后一個線程調用了該共享變量的wait()方法而被放入阻塞集合,則該線程是不會被喚醒的。嘗試把主線程里面休眠1s的代碼注釋掉,再運行程序會有一定概率輸出下面的結果。

也就是在線程B調用共享變量的wait()方法前線程C調用了共享變量的notifyAll方法,這樣,只有線程A被喚醒,而線程B并沒有被喚醒,還是處于阻塞狀態。
- ASP.NET Core:Cloud-ready,Enterprise Web Application Development
- 工程軟件開發技術基礎
- 造個小程序:與微信一起干件正經事兒
- Java加密與解密的藝術(第2版)
- HTML5+CSS3基礎開發教程(第2版)
- 跟小海龜學Python
- Hands-On C++ Game Animation Programming
- Mastering Swift 2
- JAVA程序設計實驗教程
- 大學計算機基礎實驗指導
- Corona SDK Mobile Game Development:Beginner's Guide(Second Edition)
- C#開發案例精粹
- 匯編語言編程基礎:基于LoongArch
- UI設計全書(全彩)
- 寫給程序員的Python教程