java判断线程是否wait_Java并发编程之线程间通讯(上)wait/notify机制
線程間通信
如果一個(gè)線程從頭到尾執(zhí)行完也不和別的線程打交道的話,那就不會(huì)有各種安全性問題了。但是協(xié)作越來越成為社會(huì)發(fā)展的大勢(shì),一個(gè)大任務(wù)拆成若干個(gè)小任務(wù)之后,各個(gè)小任務(wù)之間可能也需要相互協(xié)作最終才能執(zhí)行完整個(gè)大任務(wù)。所以各個(gè)線程在執(zhí)行過程中可以相互通信,所謂通信就是指相互交換一些數(shù)據(jù)或者發(fā)送一些控制指令,比如一個(gè)線程給另一個(gè)暫停執(zhí)行的線程發(fā)送一個(gè)恢復(fù)執(zhí)行的指令,下邊詳細(xì)看都有哪些通信方式。
volatile和synchronized
可變共享變量是天然的通信媒介,也就是說一個(gè)線程如果想和另一個(gè)線程通信的話,可以修改某個(gè)在多線程間共享的變量,另一個(gè)線程通過讀取這個(gè)共享變量來獲取通信的內(nèi)容。
由于原子性操作、內(nèi)存可見性和指令重排序的存在,java提供了volatile和synchronized的同步手段來保證通信內(nèi)容的正確性,假如沒有這些同步手段,一個(gè)線程的寫入不能被另一個(gè)線程立即觀測(cè)到,那這種通信就是不靠譜的~
wait/notify機(jī)制
故事背景
也不知道是那個(gè)遭天殺的給我們學(xué)校廁所的坑里塞了個(gè)塑料瓶,導(dǎo)致樓道里如黃河泛濫一般,臭味熏天。更加悲催的是整個(gè)樓只有這么一個(gè)廁所,比這個(gè)更悲催的是這個(gè)廁所里只有一個(gè)坑!!!!!好吧,讓我們用java來描述一下這個(gè)廁所:
public class Washroom {
private volatile boolean isAvailable = false; //表示廁所是否是可用的狀態(tài)
private Object lock = new Object(); //廁所門的鎖
public boolean isAvailable() {
return isAvailable;
}
public void setAvailable(boolean available) {
this.isAvailable = available;
}
public Object getLock() {
return lock;
}
}
isAvailable字段代表廁所是否可用,由于廁所損壞,默認(rèn)是false的,lock字段代表這個(gè)廁所門的鎖。需要注意的是 isAvailable字段被volatile修飾,也就是說有一個(gè)線程修改了它的值,它可以立即對(duì)別的線程可見~
由于廁所資源寶貴,英明的學(xué)校領(lǐng)導(dǎo)立即擬定了一個(gè)修復(fù)任務(wù):
public class RepairTask implements Runnable {
private Washroom washroom;
public RepairTask(Washroom washroom) {
this.washroom = washroom;
}
@Override
public void run() {
synchronized (washroom.getLock()) {
System.out.println("維修工 獲取了廁所的鎖");
System.out.println("廁所維修中,維修廁所是一件辛苦活,需要很長(zhǎng)時(shí)間。。。");
try {
Thread.sleep(5000L); //用線程sleep表示維修的過程
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
washroom.setAvailable(true); //維修結(jié)束把廁所置為可用狀態(tài)
System.out.println("維修工把廁所修好了,準(zhǔn)備釋放鎖了");
}
}
}
這個(gè)維修計(jì)劃的內(nèi)容就是當(dāng)維修工進(jìn)入廁所之后,先把門鎖上,然后開始維修,維修結(jié)束之后把Washroom的isAvailable字段設(shè)置為true,以表示廁所可用。
與此同時(shí),一群急得像熱鍋上的螞蟻的家伙在廁所門前打轉(zhuǎn)轉(zhuǎn),他們想做神馬不用我明說了吧😏😏:
public class ShitTask implements Runnable {
private Washroom washroom;
private String name;
public ShitTask(Washroom washroom, String name) {
this.washroom = washroom;
this.name = name;
}
@Override
public void run() {
synchronized (washroom.getLock()) {
System.out.println(name + " 獲取了廁所的鎖");
while (!washroom.isAvailable()) {
// 一直等
}
System.out.println(name + " 上完了廁所");
}
}
}
這個(gè)ShitTask描述了上廁所的一個(gè)流程,先獲取到廁所的鎖,然后判斷廁所是否可用,如果不可用,則在一個(gè)死循環(huán)里不斷的判斷廁所是否可用,直到廁所可用為止,然后上完廁所釋放鎖走人。
然后我們看看現(xiàn)實(shí)世界都發(fā)生了什么吧:
public class Test {
public static void main(String[] args) {
Washroom washroom = new Washroom();
new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
new Thread(new ShitTask(washroom, "貓爺"), "GRANDPA-CAT-THREAD").start();
new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();
}
}
學(xué)校先讓維修工進(jìn)入廁所維修,然后包括狗哥、貓爺、王尼妹在內(nèi)的上廁所大軍就開始圍著廁所打轉(zhuǎn)轉(zhuǎn)的旅程,我們看一下執(zhí)行結(jié)果:
維修工 獲取了廁所的鎖
廁所維修中,維修廁所是一件辛苦活,需要很長(zhǎng)時(shí)間。。。
維修工把廁所修好了,準(zhǔn)備釋放鎖了
王尼妹 獲取了廁所的鎖
王尼妹 上完了廁所
貓爺 獲取了廁所的鎖
貓爺 上完了廁所
狗哥 獲取了廁所的鎖
狗哥 上完了廁所
看起來沒有神馬問題,但是再回頭看看代碼,發(fā)現(xiàn)有兩處特別別扭的地方:
在main線程開啟REPAIR-THREAD線程后,必須調(diào)用sleep方法等待一段時(shí)間才允許上廁所線程開啟。
如果REPAIR-THREAD線程和其他上廁所線程一塊兒開啟的話,就有可能上廁所的人,比如狗哥先獲取到廁所的鎖,然后維修工壓根兒連廁所也進(jìn)不去。但是真實(shí)情況可能真的這樣的,狗哥先到了廁所,然后維修工才到。不過狗哥的處理應(yīng)該不是一直待在廁所里,而是先出來等著,啥時(shí)候維修工說修好了他再進(jìn)去。所以這點(diǎn)有些別扭~
在一個(gè)上廁所的人獲取到廁所的鎖的時(shí)候,必須不斷判斷Washroom的isAvailable字段是否為true。
如果一個(gè)人進(jìn)入到廁所發(fā)現(xiàn)廁所仍然處在不可用狀態(tài)的話,那它應(yīng)該在某個(gè)地方休息,啥時(shí)候維修工把廁所修好了,再叫一下等著上廁所的人就好了嘛,沒必要自己不停的去檢查廁所是否被修好了。
總結(jié)一下,就是一個(gè)線程在獲取到鎖之后,如果指定條件不滿足的話,應(yīng)該主動(dòng)讓出鎖,然后到專門的等待區(qū)等待,直到某個(gè)線程完成了指定的條件,再通知一下在等待這個(gè)條件完成的線程,讓它們繼續(xù)執(zhí)行。如果你覺得上邊這句話比較繞的話,我來給你翻譯一下:當(dāng)上狗哥獲取到廁所門鎖之后,如果廁所處于不可用狀態(tài),那就主動(dòng)讓出鎖,然后到等待上廁所的隊(duì)伍里排隊(duì)等待,直到維修工把廁所修理好,把廁所的狀態(tài)置為可用后,維修工再通知需要上廁所的人,然他們正常上廁所。
具體使用方式
為了實(shí)現(xiàn)這個(gè)構(gòu)想,java里提出了一套叫wait/notify的機(jī)制。當(dāng)一個(gè)線程獲取到鎖之后,如果發(fā)現(xiàn)條件不滿足,那就主動(dòng)讓出鎖,然后把這個(gè)線程放到一個(gè)等待隊(duì)列里等待去,等到某個(gè)線程把這個(gè)條件完成后,就通知等待隊(duì)列里的線程他們等待的條件滿足了,可以繼續(xù)運(yùn)行啦!
如果不同線程有不同的等待條件腫么辦,總不能都塞到同一個(gè)等待隊(duì)列里吧?是的,java里規(guī)定了每一個(gè)鎖都對(duì)應(yīng)了一個(gè)等待隊(duì)列,也就是說如果一個(gè)線程在獲取到鎖之后發(fā)現(xiàn)某個(gè)條件不滿足,就主動(dòng)讓出鎖然后把這個(gè)線程放到與它獲取到的鎖對(duì)應(yīng)的那個(gè)等待隊(duì)列里,另一個(gè)線程在完成對(duì)應(yīng)條件時(shí)需要獲取同一個(gè)鎖,在條件完成后通知它獲取的鎖對(duì)應(yīng)的等待隊(duì)列。這個(gè)過程意味著鎖和等待隊(duì)列建立了一對(duì)一關(guān)聯(lián)。
怎么讓出鎖并且把線程放到與鎖關(guān)聯(lián)的等待隊(duì)列中以及怎么通知等待隊(duì)列中的線程相關(guān)條件已經(jīng)完成java已經(jīng)為我們規(guī)定好了。我們知道,鎖其實(shí)就是個(gè)對(duì)象而已,在所有對(duì)象的老祖宗類Object中定義了這么幾個(gè)方法:
public final void wait() throws InterruptedException
public final void wait(long timeout) throws
InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void notify();
public final void notifyAll();
了解了這些方法的意思以后我們?cè)賮砀膶懸幌耂hitTask:
public class ShitTask implements Runnable {
// ... 為節(jié)省篇幅,省略相關(guān)字段和構(gòu)造方法
@Override
public void run() {
synchronized (washroom.getLock()) {
System.out.println(name + " 獲取了廁所的鎖");
while (!washroom.isAvailable()) {
try {
washroom.getLock().wait(); //調(diào)用鎖對(duì)象的wait()方法,讓出鎖,并把當(dāng)前線程放到與鎖關(guān)聯(lián)的等待隊(duì)列
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(name + " 上完了廁所");
}
}
}
看,原來我們?cè)谂袛鄮欠窨捎玫乃姥h(huán)里加了這么一段代碼:
washroom.getLock().wait();
這段代碼的意思就是讓出廁所的鎖,并且把當(dāng)前線程放到與廁所的鎖相關(guān)聯(lián)的等待隊(duì)列里。
然后我們也需要修改一下維修任務(wù):
public class RepairTask implements Runnable {
// ... 為節(jié)省篇幅,省略相關(guān)字段和構(gòu)造方法
@Override
public void run() {
synchronized (washroom.getLock()) {
System.out.println("維修工 獲取了廁所的鎖");
System.out.println("廁所維修中,維修廁所是一件辛苦活,需要很長(zhǎng)時(shí)間。。。");
try {
Thread.sleep(5000L); //用線程sleep表示維修的過程
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
washroom.setAvailable(true); //維修結(jié)束把廁所置為可用狀態(tài)
washroom.getLock().notifyAll(); //通知所有在與鎖對(duì)象關(guān)聯(lián)的等待隊(duì)列里的線程,它們可以繼續(xù)執(zhí)行了
System.out.println("維修工把廁所修好了,準(zhǔn)備釋放鎖了");
}
}
}
大家可以看出來,我們?cè)诰S修結(jié)束后加了這么一行代碼:
washroom.getLock().notifyAll();
這個(gè)代碼表示將通知所有在與鎖對(duì)象關(guān)聯(lián)的等待隊(duì)列里的線程,它們可以繼續(xù)執(zhí)行了。
在使用java的wait/notify機(jī)制修改了ShitTask和RepairTask后,我們?cè)趶?fù)原一下整個(gè)現(xiàn)實(shí)場(chǎng)景:
public class Test {
public static void main(String[] args) {
Washroom washroom = new Washroom();
new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
new Thread(new ShitTask(washroom, "貓爺"), "GRANDPA-CAT-THREAD").start();
new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();
}
}
在這個(gè)場(chǎng)景中,我們可以刻意讓著急上廁所的先到達(dá)了廁所,維修工最后抵達(dá)廁所,來看一下加了wait/notify機(jī)制的代碼的執(zhí)行結(jié)果是:
狗哥 獲取了廁所的鎖
貓爺 獲取了廁所的鎖
王尼妹 獲取了廁所的鎖
維修工 獲取了廁所的鎖
廁所維修中,維修廁所是一件辛苦活,需要很長(zhǎng)時(shí)間。。。
維修工把廁所修好了,準(zhǔn)備釋放鎖了
王尼妹 上完了廁所
貓爺 上完了廁所
狗哥 上完了廁所
從執(zhí)行結(jié)果可以看出來,狗哥、貓爺、王尼妹雖然先到達(dá)了廁所并且獲取到鎖,但是由于廁所處于不可用狀態(tài),所以都先調(diào)用wait()方法讓出了自己獲得的鎖,然后躲到與這個(gè)鎖關(guān)聯(lián)的等待隊(duì)列里,直到維修工修完了廁所,通知了在等待隊(duì)列中的狗哥、貓爺、王尼妹,他們才又開始繼續(xù)執(zhí)行上廁所的程序~
通用模式
經(jīng)過上邊的廁所案例,大家應(yīng)該對(duì)wait/notify機(jī)制有了大致了解,下邊我們總結(jié)一下這個(gè)機(jī)制的通用模式。首先看一下等待線程的通用模式:
獲取對(duì)象鎖。
如果某個(gè)條件不滿足的話,調(diào)用鎖對(duì)象的wait方法,被通知后仍要檢查條件是否滿足。
條件滿足則繼續(xù)執(zhí)行代碼。
通用的代碼如下:
synchronized (對(duì)象) {
處理邏輯(可選)
while(條件不滿足) {
對(duì)象.wait();
}
處理邏輯(可選)
}
除了判斷條件是否滿足和調(diào)用wait方法以外的代碼,其他的處理邏輯是可選的。
下邊再來看通知線程的通用模式:
獲得對(duì)象的鎖。
完成條件。
通知在等待隊(duì)列中的等待線程。
synchronized (對(duì)象) {
完成條件
對(duì)象.notifyAll();、
}
小貼士:
別忘了同步方法也是使用鎖的喔,靜態(tài)同步方法的鎖對(duì)象是該類的`Class對(duì)象`,成員同步方法的鎖對(duì)象是`this對(duì)象`。所以如果沒有刻意強(qiáng)調(diào),下邊所說的同步代碼塊也包含同步方法。
了解了wait/notify的通用模式之后,使用的時(shí)候需要特別小心,需要注意下邊這些方面:
必須在同步代碼塊中調(diào)用wait、 notify或者notifyAll方法。
有的童鞋會(huì)有疑問,為啥wait/notify機(jī)制的這些方法必須都放在同步代碼塊中才能調(diào)用呢?wait方法的意思只是讓當(dāng)前線程停止執(zhí)行,把當(dāng)前線程放在等待隊(duì)列里,notify方法的意思只是從等待隊(duì)列里移除一個(gè)線程而已,跟加鎖有什么關(guān)系?
答:因?yàn)閣ait方法是運(yùn)行在等待線程里的,notify或者notifyAll是運(yùn)行在通知線程里的。而執(zhí)行wait方法前需要判斷一下某個(gè)條件是否滿足,如果不滿足才會(huì)執(zhí)行wait方法,這是一個(gè)先檢查后執(zhí)行的操作,不是一個(gè)原子性操作,所以如果不加鎖的話,在多線程環(huán)境下等待線程和通知線程的執(zhí)行順序可能是這樣的:
也就是說當(dāng)?shù)却€程已經(jīng)判斷條件不滿足,正要執(zhí)行wait方法,此時(shí)通知線程搶先把條件完成并且調(diào)用了notify方法,之后等待線程才執(zhí)行到wait方法,這會(huì)導(dǎo)致等待線程永遠(yuǎn)停留在等待隊(duì)列而沒有人再去notify它。所以等待線程中的判斷條件是否滿足、調(diào)用wait方法和通知線程中完成條件、調(diào)用notify方法都應(yīng)該是原子性操作,彼此之間是互斥的,所以用同一個(gè)鎖來對(duì)這兩個(gè)原子性操作進(jìn)行同步,從而避免出現(xiàn)等待線程永久等待的尷尬局面。
如果不在同步代碼塊中調(diào)用wait、notify或者notifyAll方法,也就是說沒有獲取鎖就調(diào)用wait方法,就像這樣:
對(duì)象.wait();
是會(huì)拋出IllegalMonitorStateException異常的。
在同步代碼塊中,必須調(diào)用獲取的鎖對(duì)象的wait、 notify或者notifyAll方法。
也就是說不能隨便調(diào)用一個(gè)對(duì)象的wait、notify或者notifyAll方法。比如等待線程中的代碼是這樣的:
synchronized (對(duì)象1) {
while(條件不滿足) {
對(duì)象2.wait(); //隨便調(diào)用一個(gè)對(duì)象的wait方法
}
}
通知線程中的代碼是這樣的:
synchronized (對(duì)象1) {
完成條件
對(duì)象2.notifyAll();
}
對(duì)于代碼對(duì)象2.wait(),表示讓出當(dāng)前線程持有的對(duì)象2的鎖,而當(dāng)前線程持有的是對(duì)象1的鎖,所以這么寫是錯(cuò)誤的,也會(huì)拋出IllegalMonitorStateException異常的。意思就是如果當(dāng)前線程不持有某個(gè)對(duì)象的鎖,那它就不能調(diào)用該對(duì)象的wait方法來讓出該鎖。所以如果想讓等待線程讓出當(dāng)前持有的鎖,只能調(diào)用對(duì)象1.wait()。然后這個(gè)線程就被放置到與對(duì)象1相關(guān)聯(lián)的等待隊(duì)列中,在通知線程中只能調(diào)用對(duì)象1.notifyAll()來通知這些等待的線程了。
在等待線程判斷條件是否滿足時(shí),應(yīng)該使用while,而不是if。
也就是說在判斷條件是否滿足的時(shí)候要使用while:
while(條件不滿足) { //正確?
對(duì)象.wait();
}
而不是使用if:
if(條件不滿足) { //錯(cuò)誤?
對(duì)象.wait();
}
這個(gè)是因?yàn)樵诙嗑€程條件下,可能在一個(gè)線程調(diào)用notify之后立即又有一個(gè)線程把條件改成了不滿足的狀態(tài),比如在維修工把廁所修好之后通知大家上廁所吧的瞬間,有一個(gè)小屁孩以迅雷不及掩耳之勢(shì)又給廁所坑里塞了個(gè)瓶子,廁所又被置為不可用狀態(tài),等待上廁所的還是需要再判斷一下條件是否滿足才能繼續(xù)執(zhí)行。
在調(diào)用完鎖對(duì)象的notify或者notifyAll方法后,等待線程并不會(huì)立即從wait()方法返回,需要調(diào)用notify()或者notifyAll()的線程釋放鎖之后,等待線程才從wait()返回繼續(xù)執(zhí)行。。
也就是說如果通知線程在調(diào)用完鎖對(duì)象的notify或者notifyAll方法后還有需要執(zhí)行的代碼,就像這樣:
synchronized (對(duì)象) {
完成條件
對(duì)象.notifyAll();
... 通知后的處理邏輯
}
需要把通知后的處理邏輯執(zhí)行完成后,把鎖釋放掉,其他線程才可以從wait狀態(tài)恢復(fù)過來,重新競(jìng)爭(zhēng)鎖來執(zhí)行代碼。比方說在維修工修好廁所并通知了等待上廁所的人們之后,他還沒有從廁所出來,而是在廁所的墻上寫了 "XXX到此一游"之類的話之后才從廁所出來,從廁所出來才代表著釋放了鎖,狗哥、貓爺、王尼妹才開始爭(zhēng)搶進(jìn)入廁所的機(jī)會(huì)。
notify方法只會(huì)將等待隊(duì)列中的一個(gè)線程移出,而notifyAll方法會(huì)將等待隊(duì)列中的所有線程移出。
大家可以把上邊代碼中的notifyAll方法替換稱notify方法,看看執(zhí)行結(jié)果~
wait和sleep的區(qū)別
眼尖的小伙伴肯定發(fā)現(xiàn),wait和sleep這兩個(gè)方法都可以讓線程暫停執(zhí)行,而且都有InterruptedException的異常說明,那么它們的區(qū)別是啥呢?
wait是Object的成員方法,而sleep是Thread的靜態(tài)方法。
只要是作為鎖的對(duì)象都可以在同步代碼塊中調(diào)用自己的wait方法,sleep是Thread的靜態(tài)方法,表示的是讓當(dāng)前線程休眠指定的時(shí)間。
調(diào)用wait方法需要先獲得鎖,而調(diào)用sleep方法是不需要的。
在一次強(qiáng)調(diào),一定要在同步代碼塊中調(diào)用鎖對(duì)象的wait方法,前提是要獲得鎖!前提是要獲得鎖!前提是要獲得鎖!而sleep方法隨時(shí)調(diào)用~
調(diào)用wait方法的線程需要用notify來喚醒,而sleep必須設(shè)置超時(shí)值。
線程在調(diào)用wait方法之后會(huì)先釋放鎖,而sleep不會(huì)釋放鎖。
這一點(diǎn)可能是最重要的一點(diǎn)不同點(diǎn)了吧,狗哥、貓爺、王尼妹這些線程一開始是獲取到廁所的鎖了,但是調(diào)用了wait方法之后主動(dòng)把鎖讓出,從而讓維修工得以進(jìn)入廁所維修。如果狗哥在發(fā)現(xiàn)廁所是不可用的條件時(shí)選擇調(diào)用sleep方法的話,線程是不會(huì)釋放鎖的,也就是說維修工無法獲得廁所的鎖,也就修不了廁所了~ 大家一定要謹(jǐn)記這一點(diǎn)啊!
總結(jié)
線程間需要通過通信才能協(xié)作解決某個(gè)復(fù)雜的問題。
可變共享變量是天然的通信媒介,但是使用的時(shí)候一定要保證線程安全性,通常使用volatile變量或synchronized來保證線程安全性。
一個(gè)線程在獲取到鎖之后,如果指定條件不滿足的話,應(yīng)該主動(dòng)讓出鎖,然后到專門的等待區(qū)等待,直到某個(gè)線程完成了指定的條件,再通知一下在等待這個(gè)條件完成的線程,讓它們繼續(xù)執(zhí)行。這個(gè)機(jī)制就是wait/notify機(jī)制。
等待線程的通用模式:
synchronized (對(duì)象) {
處理邏輯(可選)
while(條件不滿足) {
對(duì)象.wait();
}
處理邏輯(可選)
}
可以分為下邊幾個(gè)步驟:
獲取對(duì)象鎖。
如果某個(gè)條件不滿足的話,調(diào)用鎖對(duì)象的wait方法,被通知后仍要檢查條件是否滿足。
條件滿足則繼續(xù)執(zhí)行代碼。
通知線程的通用模式:
synchronized (對(duì)象) {
完成條件
對(duì)象.notifyAll();、
}
可以分為下邊幾個(gè)步驟:
獲得對(duì)象的鎖。
完成條件。
通知在等待隊(duì)列中的等待線程。
wait和sleep的區(qū)別
wait是Object的成員方法,而sleep是Thread的靜態(tài)方法。
調(diào)用wait方法需要先獲得鎖,而調(diào)用sleep方法是不需要的。
調(diào)用wait方法的線程需要用notify來喚醒,而sleep必須設(shè)置超時(shí)值。
線程在調(diào)用wait方法之后會(huì)先釋放鎖,而sleep不會(huì)釋放鎖。
總結(jié)
以上是生活随笔為你收集整理的java判断线程是否wait_Java并发编程之线程间通讯(上)wait/notify机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux登录界面输入密码时卡住6,ce
- 下一篇: java 注解与反射_Java注解与反射