Java并發(fā)編程(五)---線程通信
前言
上一篇我們介紹了死鎖的發(fā)生條件,以及避免死鎖的方式。其中 破壞占有且等待的處理是,通過一個單例類一次性申請所有資源,直到成功。如while (!Allocator.getAllocator().applyResource(this, target)) { return; } 如果在并發(fā)量比較小的情況下,還可以接受,如果并發(fā)量比較大的話,就會大量的消耗CPU的資源。這時候,我們應(yīng)該引入線程通信,主要是 等待-喚醒機(jī)制。
線程通信
下面我們通過一個例子來說明。場景說明:
圖書館里,有一本書叫《Java 高并發(fā)實(shí)戰(zhàn)》,小A早上的時候把這本書借走了,小B中午的時候去圖書館找這本書,這里小A和小B分別是兩個線程,他們都要看的書是共享資源。
通過共享對象通信
小B去了圖書館,發(fā)現(xiàn)這本書被借走了,他回到家,等了幾天,再去圖書館找這本書,發(fā)現(xiàn)這本書已經(jīng)被歸還了,他順利借走了書。
用程序模擬的話,就是線程小A調(diào)用setCanBorrow方法將canBorrow設(shè)為true,線程小B調(diào)用getCanBorrow獲取。在這里線程A和B必須獲得
指向同一個LibraryTest1共享實(shí)例的引用,如果持有的對象指向不同的LibraryTest1實(shí)例,那么彼此將不能檢測到對方的信號。
public class LibraryTest1 {
//是否可借
private boolean canBorrow = false;
synchronized boolean getCanBorrow() {
return canBorrow;
}
synchronized void setCanBorrow(boolean canBorrow) {
this.canBorrow = canBorrow;
}
}
忙等待
其實(shí)小A在小B走后一會就把書還回去了,小B卻在幾天后才去找書,為了早點(diǎn)借到書(減少延遲),小B可能在圖書館等著,每隔幾分鐘(while循環(huán)),他就去檢查這本書有沒有被還回,這樣只要小A一還回書,小B馬上就會知道。
public class LibraryTest1 {
final LibraryTest1 libraryTest1 = new LibraryTest1();
while (!libraryTest1.getCanBorrow()) {
//空等
return;
}
}
wait(),notify()和notifyAll()
很多次后,小B發(fā)現(xiàn)自己這樣做太累了,身體有點(diǎn)吃不消,不過很快,學(xué)校圖書館系統(tǒng)改進(jìn),加入了短信通知功能(notify()),只要小A一還回書,圖書館會立馬通知小B,這樣小B就可以在家睡覺等短信了。
//是否可借
private boolean canBorrow = false;
synchronized String borrowBook() throws InterruptedException {
if (!canBorrow) {
wait();
return null;
}
canBorrow = false;
return "Java 高并發(fā)實(shí)戰(zhàn)";
}
synchronized void giveBackBook() {
this.canBorrow = true;
notify();
}
如上程序,canBorrow 初始為false時(書本已經(jīng)被小A借走),小A 還書(調(diào)用giveBackBook方法)之后,書的可借狀態(tài)為可借,并且調(diào)用notify()通知等待線程(系統(tǒng)發(fā)短信給小B)需要注意的兩點(diǎn)是:
wait(),notify() 和或者notifyAll() 都需要在同步代碼塊中調(diào)用(就是消息只能由圖書管理系統(tǒng)發(fā)出),不能再同步代碼塊之外調(diào)用,否則,會拋出IllegalMonitorStateException異常。
notify() 是隨機(jī)的通知等待隊(duì)列里的某一個線程,而notifyAll()是通知等待隊(duì)列里的所有線程。一般而言最好調(diào)用notifyAll(),而不要調(diào)用notify()。因?yàn)?,通知時,只是表示通知時間點(diǎn)條件滿足,等線程執(zhí)行時,條件可能已經(jīng)不滿足了,線程的執(zhí)行時間與通知時間不重合,如果調(diào)用notify()的話很快能不能通知到我們期望通知的線程。
wait()與sleep() 的相同點(diǎn)與不同點(diǎn)
相同點(diǎn):都會讓當(dāng)前線程掛起一段時間,讓渡CPU的執(zhí)行時間,等待再次調(diào)度
不同點(diǎn):
wait(),notify(),notifyAll()一定是在synchronized{}內(nèi)部調(diào)用,等待和通知的對象必須要對應(yīng)。而sleep可以再任何地方調(diào)用
wait 會釋放鎖對象的“鎖標(biāo)志”,當(dāng)調(diào)用某一對象的wait()方法后,會使當(dāng)前線程暫停執(zhí)行,并將當(dāng)前線程放入對象等待池中。知道調(diào)用notifyAll()方法,而sleep則不會釋放,也就是說在休眠期間,其他線程仍然不能訪問共享數(shù)據(jù)。
wait可以被喚醒,sleep的只能等其睡眠結(jié)束
wait()是在Object 類里,而sleep是在Thread 里的
丟失的信號
學(xué)校圖書館系統(tǒng)是這么設(shè)計(jì)的,當(dāng)一本書被還回來的時候,會給等待著發(fā)送短信,并且只會發(fā)一次,如果沒有等待者,他也會發(fā)(只不過沒有接受者)。問題出現(xiàn)了,因?yàn)槎绦胖粫l(fā)一次,當(dāng)書被還回來的時候,沒有人等待借書,他會發(fā)一條空短信,但是之后有等待借此本書的同學(xué)永遠(yuǎn)也不會再收到短信,導(dǎo)致這些同學(xué)會無休止的等待,為了避免這個問題,我們等待的時候先打個電話問問圖書管理員是否繼續(xù)等待if(!wasSignalled)。
public class LibraryTest3 {
private MonitorObject monitorObject = new MonitorObject();
private boolean canBorrow = false;
private boolean wasSignalled = false;
String borrowBook() throws InterruptedException {
synchronized (monitorObject) {
if (!canBorrow||!wasSignalled) {
wait();
return null;
}
canBorrow = false;
return "Java 高并發(fā)實(shí)戰(zhàn)";
}
}
void giveBackBook() {
synchronized (monitorObject) {
this.canBorrow = true;
this.wasSignalled = true;
notifyAll();
}
}
}
notify()和notifyAll()方法不會保存調(diào)用它們的方法,因?yàn)楫?dāng)這兩個方法被調(diào)用時,有可能沒有線程處于等待狀態(tài),通知信號過后便丟棄了。因此,如果一個線程先于被通知線程調(diào)用wait()前調(diào)用notify(),等待的線程就將錯過這個信號。在某些情況下,這可能使得等待線程永久等待,不再被喚醒。所以,為了避免丟失信號,我們需要將信號保存到信號類里。這里的管理員就是信號類。
假喚醒
圖書館系統(tǒng)還有一個BUG:系統(tǒng)會偶爾給你發(fā)條錯誤短信,說書可以借了(其實(shí)不可以借),我們之前已經(jīng)該圖書館管理員打過電話了,他說讓我們等短信。我們很聽話,一等到短信(其實(shí)是bug引起的錯誤短信),就去借書了,到了圖書館后發(fā)現(xiàn)這書根本沒有還回來!我們很郁悶,但也沒有辦法呀,學(xué)校不修復(fù)BUG,我們得聰明點(diǎn):每次在收到短信后,再打電話問問書到底能不能借while(!canBorrow||!wasSignalled)。
String borrowBook() throws InterruptedException {
synchronized (monitorObject) {
while (!canBorrow||!wasSignalled) {
wait();
return null;
}
canBorrow = false;
return "Java 高并發(fā)實(shí)戰(zhàn)";
}
}
就像案例里說的,有時候線程會被莫名奇妙的喚醒,為了防止假喚醒,保存信號的成員變量將在一個while循環(huán)里接收檢查。而不是在if表達(dá)式里。
不要對常量字符串或全局對象調(diào)用wait()
因?yàn)槿绻褂贸A孔址脑?。JVM/編譯器內(nèi)部會把常量字符串轉(zhuǎn)成同一個對象,
這意味著即使你有2個不同的MyWaitNotify3實(shí)例,它們都是引用了相同的空字符串實(shí)例。同時也意味著存在這樣的風(fēng)險(xiǎn),在第一個MyWaitNotify3實(shí)例調(diào)用doWait()會被第二個MyWaitNotify3實(shí)例上調(diào)用doNotify()的線程喚醒。
public class MyWaitNotify3 {
String monitorObject = "";
boolean isSignaled = false;
public void doWait() {
synchronized (monitorObject) {
while (!isSignaled) {
try {
monitorObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//清楚標(biāo)記,繼續(xù)執(zhí)行
isSignaled = false;
}
}
public void doNotify() {
synchronized (monitorObject) {
isSignaled = true;
monitorObject.notifyAll();
}
}
}
總結(jié)
本文主要介紹了線程之間的通信,通過一個現(xiàn)實(shí)中的場景再現(xiàn)了這5種場景,其中等待-通知機(jī)制是線程通信的核心機(jī)制。工作中用輪詢的方式來等待某個狀態(tài),很多情況下都可以用等待-通知機(jī)制。
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥