新聞中心
本文章收錄于《Java并發(fā)編程》合集中,本篇來(lái)介紹線程間通信,線程間通信 使線程成為一個(gè)整體,提高系統(tǒng)之間的 交互性,在提高CPU利用率的同時(shí)可以對(duì)線程任務(wù)進(jìn)行有效的把控與監(jiān)督。

創(chuàng)新互聯(lián)公司-云計(jì)算及IDC服務(wù)提供商,涵蓋公有云、IDC機(jī)房租用、遂寧托管服務(wù)器、等保安全、私有云建設(shè)等企業(yè)級(jí)互聯(lián)網(wǎng)基礎(chǔ)服務(wù),聯(lián)系電話:18980820575
比如:多線程之間交替執(zhí)行,多線程按順序執(zhí)行等,都需要使用線程通信技術(shù),通過(guò)本篇文章您可以獲得:
什么是線程通信,有什么作用
線程通信的三種實(shí)現(xiàn)方式
notifyAll的虛假喚醒問(wèn)題,notify死鎖問(wèn)題
通過(guò) ReentrantLock 實(shí)現(xiàn)精確喚醒
多線程按順序執(zhí)行的四種方案
線程通信常見(jiàn)面試題解析
相信你還有更多方式實(shí)現(xiàn)線程通信?不妨評(píng)論區(qū)告訴我們吧,高頻率碼字不易,覺(jué)得文章不錯(cuò)記得點(diǎn)贊支持一下哦!
線程間通信
線程之間的交互我們稱之為線程通信【Inter-Thread Communication,簡(jiǎn)稱ITC】,指多個(gè)線程處理同一資源,但是任務(wù)不同
比如:小明放假在家,肚子餓了,如果發(fā)現(xiàn)沒(méi)有吃的就會(huì)喊:媽,我餓了,弄點(diǎn)吃的,如果媽媽發(fā)現(xiàn)沒(méi)有吃的了就會(huì)做菜,通知小明吃飯,總之:有菜通知小明吃飯,沒(méi)菜小明通知媽媽做飯,簡(jiǎn)直吃貨一個(gè)
此時(shí)就是兩個(gè)線程對(duì)飯菜這同一資源有不同的任務(wù),媽媽線程就是做飯,小明線程是吃飯,如果想要實(shí)現(xiàn)上邊的場(chǎng)景,就需要媽媽線程和小明線程之間通信
要實(shí)現(xiàn)線程之間通信一般有三種方法:
- 使用Object對(duì)象鎖的wait()、notify()和notifyAll()方法
- 使用Java5新增的JUC中的ReentrantLock結(jié)合Condition
- 使用JUC中的CountDownLatch【計(jì)數(shù)器】
對(duì)象鎖wait和notifyAll方法實(shí)現(xiàn)
在此案例中,同一資源就是飯菜,小明對(duì)吃的操作是造,而媽媽對(duì)吃的操作是做
飯菜資源:
public class KitChenRoom {
// 是否有吃的
private boolean hasFood = false;
// 設(shè)置同步鎖,做飯和吃飯只能同時(shí)有一個(gè)在執(zhí)行,不能邊做邊吃
private Object lock = new Object();
// 做飯
public void cook() {
// 加鎖
synchronized (lock) {
// 如果有吃的,就不做飯
if(hasFood) {
// 還有吃的,先不做飯
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就做飯,
System.out.println(Thread.currentThread().getName() + "沒(méi)吃的了,給娃做飯!");
// 做好之后,修改為true
hasFood = true;
// 通知其他線程吃飯
lock.notifyAll();
}
}
// 吃飯
public void eat() {
synchronized (lock) {
// 如果沒(méi)吃的,就喊媽媽做飯,暫時(shí)吃不了
if (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就吃飯
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
// 吃完之后,修改為false
hasFood = false;
// 通知其他線程吃飯
lock.notifyAll();
}
}
}測(cè)試類:
public class KitChenMain {
public static void main(String[] args) {
// 創(chuàng)建飯菜對(duì)象
KitChenRoom chenRoom = new KitChenRoom();
// 創(chuàng)建媽媽線程,做飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"媽媽線程:").start();
// 創(chuàng)建小明線程,吃飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"小明線程:").start();
}
}運(yùn)行結(jié)果:發(fā)現(xiàn)兩個(gè)線程交替執(zhí)行,沒(méi)飯的時(shí)候媽媽做飯,有飯的時(shí)候小明就恰飯
虛假喚醒
在wait方法的源碼注釋中有這么一段話:
As in the one argument version, interrupts and spurious wakeups are possible,
and this method should always be used in a loop
翻譯:在單參數(shù)版本中,中斷和虛假喚醒是可能的,并且該方法應(yīng)始終在循環(huán)中使用
比如上邊的 飯菜資源 代碼中我們使用的是if判斷是否有吃的
如果此時(shí)我們?cè)匍_(kāi)啟一個(gè)大明線程吃飯,開(kāi)啟一個(gè)爸爸線程做飯,此時(shí)會(huì)發(fā)生什么問(wèn)題呢
改造測(cè)試類:再開(kāi)啟一個(gè)大明線程和一個(gè)爸爸線程
public class KitChenMain {
public static void main(String[] args) {
KitChenRoom chenRoom = new KitChenRoom();
// 創(chuàng)建媽媽線程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"媽媽線程:").start();
// 創(chuàng)建小明線程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"小明線程:").start();
// 爸爸線程:做飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"爸爸線程:").start();
// 大明線程:吃飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"大明線程:").start();
}
}運(yùn)行結(jié)果:發(fā)現(xiàn)爸爸線程和媽媽線程連著做了三次飯
原因:
- 這是由于wait方法的機(jī)制導(dǎo)致的,wait方法會(huì)使線程阻塞,直到被喚醒之后才會(huì)運(yùn)行,在哪里阻塞,再次被喚醒之后得到CPU執(zhí)行權(quán),就會(huì)在哪里繼續(xù)運(yùn)行
- 現(xiàn)在是4條線程,假設(shè)爸爸線程運(yùn)行之后將 hasFood 改為true,此時(shí)爸爸線程就會(huì)喚醒其他線程,也就是媽媽線程和小明,大明線程都會(huì)被喚醒,如果此時(shí)媽媽線程獲取到CPU時(shí)間片開(kāi)始運(yùn)行,判斷 hasFood 為 true,那么就觸發(fā)wait等待,等待之后就會(huì)釋放CPU執(zhí)行權(quán),喚醒其他線程
- 如果此時(shí)爸爸線程又獲取到CPU執(zhí)行權(quán),同樣判斷hasFood之后為true,就會(huì)進(jìn)入等待,喚醒其他線程,如果此時(shí)CPU執(zhí)行權(quán)又分配給了媽媽線程,因?yàn)橹耙呀?jīng)經(jīng)過(guò)了判斷,就會(huì)在wait的地方,繼續(xù)執(zhí)行,就會(huì)觸發(fā)給娃做飯,之后再喚醒其他線程
- 此時(shí)爸爸線程得到CPU時(shí)間片,則會(huì)在上次wait的地方繼續(xù)執(zhí)行,同樣的給娃做飯,就會(huì)出現(xiàn)上圖的效果,爸媽線程交替做飯
解決:將if替換為while,while語(yǔ)句塊每次執(zhí)行完之后都會(huì)重新判斷,知道條件不成立才會(huì)結(jié)束循環(huán),即可解決
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
// 加鎖
synchronized (lock) {
// 將if替換為while
while(hasFood) {
// 還有吃的,先不做飯
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就做飯,
System.out.println(Thread.currentThread().getName() + "沒(méi)吃的了,給娃做飯!");
// 做好之后,修改為true
hasFood = true;
// 通知其他線程吃飯
lock.notifyAll();
}
}
// 吃飯
public void eat() {
synchronized (lock) {
// 將if替換為while
while (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就吃飯
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
// 吃完之后,修改為false
hasFood = false;
// 通知其他線程吃飯
lock.notifyAll();
}
}
}運(yùn)行結(jié)果:發(fā)現(xiàn)做飯和吃飯交替執(zhí)行
為什么使用while就能解決呢?其實(shí)就是 if和while的區(qū)別
由于在多線程內(nèi)容中,有很多小伙伴犯迷,為什么用while就解決了,其實(shí)是思路沒(méi)有打開(kāi),把以前學(xué)的東西都忘記了,滿腦子都是多線程的東西,你說(shuō)是不是!學(xué)習(xí)要融會(huì)貫通,將前后所有的知識(shí)點(diǎn)串起來(lái)
解決虛假喚醒非常簡(jiǎn)單,其實(shí)就是利用了while的特性,while體每次執(zhí)行都會(huì)循環(huán)再次判斷條件,直到條件不成立跳出循環(huán),在這也是一樣:
- 媽媽線程執(zhí)行發(fā)現(xiàn)hasFood = true,就進(jìn)入等待,再次得到cpu時(shí)間片執(zhí)行時(shí),在哪里等待就在哪里醒來(lái)繼續(xù)執(zhí)行,也就是再lock.wait()的地方繼續(xù)執(zhí)行
- 由于該代碼在while循環(huán)中,會(huì)循環(huán)判斷,如果hasFood = true繼續(xù)wait,如果hasFood = false就跳出循環(huán),執(zhí)行循環(huán)體之外的代碼
- 但是如果是if,就只會(huì)判斷一次,醒來(lái)之后不會(huì)再次判斷,因?yàn)閘ock.wait()代碼已經(jīng)執(zhí)行過(guò)了,會(huì)直接向下執(zhí)行,開(kāi)始給娃做飯
notify和notifyAll
上邊我們使用notifyAll喚醒了所有線程,如果將notifyAll替換為notify會(huì)發(fā)生什么?
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
synchronized (lock) {
while (hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "沒(méi)吃的了,給娃做飯!");
hasFood = true;
// // 替換為notify
lock.notify();
}
}
public void eat() {
synchronized (lock) {
while (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
hasFood = false;
// 替換為notify
lock.notify();
}
}
}運(yùn)行結(jié)果:運(yùn)行三次,發(fā)現(xiàn)前兩次程序卡住不動(dòng),產(chǎn)生死鎖,第三次正常執(zhí)行完
在解釋這個(gè)原因之前先搞清楚 鎖池 和 等待池 兩個(gè)概念:
- 鎖池:假設(shè)線程A已經(jīng)擁有了某個(gè)對(duì)象的鎖【注意:不是類】,而其它的線程想要調(diào)用這個(gè)對(duì)象的某個(gè)synchronized方法【或者synchronized塊】,由于這些線程在進(jìn)入對(duì)象的synchronized方法之前必須先獲得該對(duì)象的鎖的擁有權(quán),但是該對(duì)象的鎖目前正被線程A擁有,所以這些線程就進(jìn)入了該對(duì)象的鎖池中。
- 等待池:假設(shè)一個(gè)線程A調(diào)用了某個(gè)對(duì)象的wait()方法,線程A就會(huì)釋放該對(duì)象的鎖,之后進(jìn)入到了該對(duì)象的等待池中
對(duì)象鎖:任何一個(gè)對(duì)象都可以被當(dāng)做鎖,所以稱為對(duì)象鎖,比如下方代碼lock1和lock2就是兩把對(duì)象鎖,都有自己獨(dú)立的鎖池和等待池
- 調(diào)用 lock1.wait() 就是該線程進(jìn)入到lock1對(duì)象鎖的等待池中
- lock1.notify()就是喚醒lock1對(duì)象鎖的等待池中的隨機(jī)一個(gè)等待線程,lock1.notifyAll(); 就是喚醒該等待池中所有等待線程
- lock1的鎖池和等待池與lock2是獨(dú)立的,互不影響,并不會(huì)喚醒彼此等待池中的線程
// 鎖1
private Object lock1 = new Object();
// 鎖2
private Object lock2 = new Object();
public void cook() {
// 使用lock1對(duì)象鎖
synchronized (lock1) {
lock1.wait();
}
lock1.notify();
}
調(diào)用wait、notify、notifyAll之后線程變化:
- 如果線程調(diào)用了對(duì)象的wait()方法,那么線程便會(huì)處于該對(duì)象的等待池中,等待池中的線程不會(huì)去競(jìng)爭(zhēng)該對(duì)象的鎖。
- 當(dāng)有線程調(diào)用了對(duì)象的notifyAll()方法【喚醒所有該對(duì)象等待池中的 wait 線程】或 notify()方法【只隨機(jī)喚醒一個(gè)該對(duì)象等待池中的 wait 線程】,被喚醒的的線程便會(huì)進(jìn)入該對(duì)象的鎖池中,鎖池中的線程會(huì)去競(jìng)爭(zhēng)該對(duì)象鎖。也就是說(shuō),調(diào)用了notify后只要一個(gè)線程會(huì)由等待池進(jìn)入鎖池,而notifyAll會(huì)將該對(duì)象等待池內(nèi)的所有線程移動(dòng)到鎖池中,等待鎖競(jìng)爭(zhēng)
- 優(yōu)先級(jí)高的線程競(jìng)爭(zhēng)到對(duì)象鎖的概率大,假若某線程沒(méi)有競(jìng)爭(zhēng)到該對(duì)象鎖,它還會(huì)留在鎖池中,唯有線程再次調(diào)用wait()方法,它才會(huì)重新回到等待池中。而競(jìng)爭(zhēng)到對(duì)象鎖的線程則繼續(xù)往下執(zhí)行,直到執(zhí)行完了 synchronized 代碼塊,它會(huì)釋放掉該對(duì)象鎖,這時(shí)鎖池中的線程會(huì)繼續(xù)競(jìng)爭(zhēng)該對(duì)象鎖。
為什么會(huì)死鎖呢?
KitChenRoom中有 cook 和 eat 兩個(gè)方法都是有同步代碼塊,并且進(jìn)入while之后就會(huì)調(diào)用lock對(duì)象鎖的wait方法,所以多個(gè)調(diào)用過(guò)cook和eat方法的線程就會(huì)進(jìn)入等待池處于阻塞狀態(tài),等待一個(gè)正在運(yùn)行的線程來(lái)喚醒它們。下面分別分析一下使用notify和notifyAll方法喚醒線程的不同之處:
- 使用notify:notify方法只能喚醒一個(gè)線程,其它等待的線程仍然處于wait狀態(tài),假設(shè)調(diào)用cook方法的線程執(zhí)行完后,所有的線程都處于等待狀態(tài),此時(shí)又執(zhí)行了notify方法,這時(shí)如果喚醒的仍然是一個(gè)調(diào)用cook方法的線程【比如爸爸線程 將 媽媽線程喚醒】,那么while循環(huán)等于true,則此喚醒的線程【媽媽線程】就會(huì)調(diào)用wait方法,也會(huì)處于等待狀態(tài),而且沒(méi)有喚醒其他線程,那就芭比Q了,此時(shí)所有的線程都處于等待狀態(tài),就發(fā)生了死鎖。
- 使用notifyAll:可以喚醒所有正在等待該鎖的線程,那么所有的線程都會(huì)處于運(yùn)行前的準(zhǔn)備狀態(tài)(就是cook方法執(zhí)行完后,喚醒了所有等待該鎖的線程),那么此時(shí),即使再次喚醒一個(gè)調(diào)用cook方法的線程,while循環(huán)等于true,喚醒的線程再次處于等待狀態(tài),那么還會(huì)有其它的線程可以獲得鎖,進(jìn)入運(yùn)行狀態(tài)。
解決wait死鎖的兩種方案:
- 通過(guò)調(diào)用notifyAll喚醒所有等待線程
- 調(diào)用 wait(long timeout) 重載方法,設(shè)置等待超時(shí)時(shí)長(zhǎng),在指定時(shí)間內(nèi)還沒(méi)被喚醒則自動(dòng)醒來(lái)
下邊仍然是調(diào)用 notify 喚醒等待池中的一個(gè)線程,但是調(diào)用wait(long timeout) 超時(shí)等待方法,讓線程進(jìn)入等待狀態(tài)
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
synchronized (lock) {
while (hasFood) {
try {
// 超時(shí)等待 2 秒
lock.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "沒(méi)吃的了,給娃做飯!");
hasFood = true;
lock.notify();
}
}
public void eat() {
synchronized (lock) {
while (!hasFood) {
try {
// 超時(shí)等待 2 秒
lock.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
hasFood = false;
lock.notify();
}
}
}運(yùn)行結(jié)果:運(yùn)行三次發(fā)現(xiàn),第一次程序陷入了兩次等待2秒之后程序繼續(xù)執(zhí)行,這就是超時(shí)自動(dòng)喚醒,避免了死鎖
總結(jié):
- notify方法很容易引起死鎖,除非你根據(jù)自己的程序設(shè)計(jì),確定不會(huì)發(fā)生死鎖,notifyAll方法則是線程的安全喚醒方法
- 如果程序允許超時(shí)喚醒,則可以使用wait(long timeout)方法
- wait(long timeout,int nanou):與 wait(long timeout)相同,不過(guò)提供了納秒級(jí)別的更精確的超時(shí)控制
ReentrantLock結(jié)合Condition
Condition是JDK1.5新增的接口,在java.util.concurrent.locks 包中,提供了類似的Object的監(jiān)視器方法,與Lock配合可以實(shí)現(xiàn)等待/通知模式,方法作用在下方源碼中已簡(jiǎn)單注釋,想要查看詳細(xì)說(shuō)明,強(qiáng)烈建議看源碼,通過(guò)翻譯軟件翻譯一下就行!
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Date;
public interface Condition {
//使當(dāng)前線程在接到信號(hào)或被中斷之前一直處于等待狀態(tài)
void await() throws InterruptedException;
// 使當(dāng)前線程在接到信號(hào)之前一直處于等待狀態(tài)?!咀⒁猓涸摲椒▽?duì)中斷不敏感】。
void awaitUninterruptibly();
// 使當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定等待時(shí)間之前一直處于等待狀態(tài)。
// 返回值表示剩余時(shí)間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時(shí)間,如果返回值 <= 0 ,則可以認(rèn)定它已經(jīng)超時(shí)了
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 使當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定等待時(shí)間之前一直處于等待狀態(tài)
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 使當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定最后期限之前一直處于等待狀態(tài)。如果沒(méi)有到指定時(shí)間就被通知,則返回true,否則表示到了指定時(shí)間,返回返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
// 喚醒一個(gè)等待線程。該線程從等待方法返回前必須獲得與Condition相關(guān)的鎖。
void signal();
// 喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關(guān)的鎖
void signalAll();
}
在此我們通過(guò)經(jīng)典的生產(chǎn)者消費(fèi)者案例說(shuō)一下Condition實(shí)現(xiàn)線程通信,多幾種案例思維更寬闊,多樣化理解對(duì)技術(shù)刺激更大
案例:有一個(gè)快遞點(diǎn),可以接貨和送貨,最多存放5個(gè)包裹,再放就提示包裹已滿,派件時(shí)包裹送完就不能再送,提示沒(méi)有包裹,不能派送
快遞點(diǎn):
package com.stt.thread.communication;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 快遞點(diǎn):
* goodsNumber: 快遞數(shù)量,默認(rèn)為0,最多5個(gè),保障原子性使用 AtomicInteger
* receiving() : 收貨方法,累加貨物數(shù)量,每次 + 1
* dispatch() : 派送方法,遞減數(shù)量,每次 - 1
* 注意:因?yàn)槭褂?Condition 實(shí)現(xiàn),Condition 需要通過(guò) ReentrantLock 獲取,
* 所以可以使用 ReentrantLock實(shí)現(xiàn)同步就不需要 synchronized
*/
public class ExpressPoint {
// 快遞數(shù)量,使用原子類
private AtomicInteger goodsNumber = new AtomicInteger();
// 鎖對(duì)象
private ReentrantLock lock = new ReentrantLock();
// 創(chuàng)建線程通信對(duì)象
private Condition condition = lock.newCondition();
// 收貨方法,使用Lock鎖,就不需要synchronized同步了
public void receiving() {
// 上鎖
lock.lock();
// 寫try...finally,保障無(wú)論是否發(fā)生異常都可以解鎖,避免死鎖
try {
// 如果達(dá)到5個(gè),就提示,并且等待
while (goodsNumber.get() == 5) {
System.out.println("庫(kù)房已滿,已不能再接收!");
// 等待,有異常拋出
condition.await();
}
System.out.println(Thread.currentThread().getName() + "已收到編號(hào):" + goodsNumber.incrementAndGet() + "的包裹");
// 喚醒其他線程
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
// 派送方法
public void dispatch() {
// 上鎖
lock.lock();
try {
// 等于0就不能再派送
while (goodsNumber.get() == 0) {
System.out.println("沒(méi)有包裹,不能派送!");
condition.await();
}
System.out.println(Thread.currentThread().getName() + "已送出編號(hào):" + goodsNumber.get() + "的包裹");
goodsNumber.decrementAndGet();
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
}
測(cè)試類:通過(guò)while死循環(huán),不斷接貨和送貨
public class ExpressPointMain {
public static void main(String[] args) {
ExpressPoint expressPoint = new ExpressPoint();
// 收貨線程
new Thread(() -> {
while (true){
expressPoint.receiving();
}
},"收貨員").start();
// 送貨線程
new Thread(() -> {
while (true){
expressPoint.dispatch();
}
},"送貨員").start();
}
}運(yùn)行結(jié)果:發(fā)現(xiàn)收貨員線程和送貨員線程交替執(zhí)行,并且?guī)齑鏉M和送完之后都有對(duì)應(yīng)的提示
總結(jié):在Condition中,用await()替換wait(),用signal()替換 notify(),用signalAll()替換notifyAll(),對(duì)于我們以前使用傳統(tǒng)的Object方法,Condition都能夠給予實(shí)現(xiàn)
Condition 精準(zhǔn)喚醒
不同的 Condition 可以用來(lái)等待和喚醒不同的線程,類似于上邊我們說(shuō)的等待池,但是Condition是通過(guò)隊(duì)列實(shí)現(xiàn)等待和喚醒,Condition的await()方法,會(huì)使得當(dāng)前線程進(jìn)入等待隊(duì)列并釋放鎖,同時(shí)線程狀態(tài)變?yōu)榈却隣顟B(tài)。當(dāng)從await()返回時(shí),當(dāng)前線程一定是獲取了Condition相關(guān)聯(lián)的鎖。Condition實(shí)現(xiàn)方式在后邊我們?cè)俜治?/p>
上邊調(diào)用await 和 signalAll方法是控制所有該Condition對(duì)象的線程,我們有兩個(gè)線程分別為收貨和送貨,我們可以創(chuàng)建兩個(gè)Condition對(duì)象來(lái)精準(zhǔn)控制等待和喚醒收貨和送貨線程。
package com.stt.thread.communication;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
定義兩個(gè) Condition 對(duì)象,一個(gè)控制收貨線程等待和喚醒,一個(gè)控制送貨線程的等待和喚醒
*/
public class ExpressPoint {
// 快遞數(shù)量,使用原子類
private AtomicInteger goodsNumber = new AtomicInteger();
// 鎖對(duì)象
private ReentrantLock lock = new ReentrantLock();
// 創(chuàng)建線程通信對(duì)象
private Condition receivingCondition = lock.newCondition();
private Condition dispatchCondition = lock.newCondition();
// 收貨方法,使用Lock鎖,就不需要synchronized同步了
public void receiving() {
// 上鎖
lock.lock();
// 寫try...finally,保障無(wú)論是否發(fā)生異常都可以解鎖,避免死鎖
try {
// 判斷是否繼續(xù)接貨
while (goodsNumber.get() == 5) {
System.out.println("庫(kù)房已滿,已不能再接收!");
// 讓收貨線程進(jìn)入等待
receivingCondition.await();
}
System.out.println(Thread.currentThread().getName() + "已收到編號(hào):" + goodsNumber.incrementAndGet() + "的包裹");
// 僅僅喚醒送貨線程
dispatchCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
// 派送方法
public void dispatch() {
// 上鎖
lock.lock();
try {
// 判斷是否繼續(xù)送貨
while (goodsNumber.get() == 0) {
System.out.println("沒(méi)有包裹,不能派送!");
// 送貨線程等待
dispatchCondition.await();
}
System.out.println(Thread.currentThread().getName() + "已送出編號(hào):" + goodsNumber.get() + "的包裹");
goodsNumber.decrementAndGet();
// 喚醒收貨線程
receivingCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
}
運(yùn)行結(jié)果:運(yùn)行結(jié)果是一樣的,只是僅僅會(huì)讓對(duì)應(yīng)的線程等待和喚醒
Condition實(shí)現(xiàn)分析
等待隊(duì)列
Conditiont的等待隊(duì)列是一個(gè)FIFO隊(duì)列,隊(duì)列的每個(gè)節(jié)點(diǎn)都是等待在Condition對(duì)象上的線程的引用,該線程就是在Condition對(duì)象上等待的線程,如果一個(gè)線程調(diào)用了Condition.await(),那么該線程就會(huì)釋放鎖,構(gòu)成節(jié)點(diǎn)加入等待隊(duì)列并進(jìn)入等待狀態(tài)。
從下圖可以看出來(lái)Condition擁有首尾節(jié)點(diǎn)的引用,而新增節(jié)點(diǎn)只需要將原有的尾節(jié)點(diǎn)nextWaiter指向它,并更新尾節(jié)點(diǎn)即可。上述節(jié)點(diǎn)引用更新過(guò)程沒(méi)有使用CAS機(jī)制,因?yàn)樵谡{(diào)用await()的線程必定是獲取了鎖的線程,該過(guò)程由鎖保證線程的安全。
一個(gè)Lock(同步器)擁有一個(gè)同步隊(duì)列和多個(gè)等待隊(duì)列:
如上邊的例子:就是擁有receivingCondition 和 dispatchCondition兩個(gè)等待隊(duì)列
private Condition receivingCondition = lock.newCondition();
private Condition dispatchCondition = lock.newCondition();
等待
調(diào)用Condition的await()方法,會(huì)使得當(dāng)前線程進(jìn)入等待隊(duì)列并釋放鎖,同時(shí)線程狀態(tài)變?yōu)榈却隣顟B(tài)。當(dāng)從await()返回時(shí),當(dāng)前線程一定是獲取了Condition相關(guān)聯(lián)的鎖。
線程觸發(fā)await()這個(gè)過(guò)程可以看作是同步隊(duì)列的首節(jié)點(diǎn)【當(dāng)前線程肯定是成功獲得了鎖,才會(huì)執(zhí)行await方法,因此一定是在同步隊(duì)列的首節(jié)點(diǎn)】移動(dòng)到了Condition的等待隊(duì)列的尾節(jié)點(diǎn),并釋放同步狀態(tài)進(jìn)入等待狀態(tài),同時(shí)會(huì)喚醒同步隊(duì)列的后繼節(jié)點(diǎn)
喚醒
- 調(diào)用signal():會(huì)喚醒再等待隊(duì)列中的首節(jié)點(diǎn),該節(jié)點(diǎn)也是到目前為止等待時(shí)間最長(zhǎng)的節(jié)點(diǎn)
- 調(diào)用signalAll():將等待隊(duì)列中的所有節(jié)點(diǎn)全部喚醒,相當(dāng)于將等待隊(duì)列中的每一個(gè)節(jié)點(diǎn)都執(zhí)行一次signal()
CountDownLatch
Java5之后在 java.util.concurrent 也就是【JUC】包中提供了很多并發(fā)編程的工具類,如 CountDownLatch 計(jì)數(shù)器是基于 AQS 框架實(shí)現(xiàn)的多個(gè)線程之間維護(hù)共享變量的類
使用場(chǎng)景
可以通過(guò) CountDownLatch 使當(dāng)前線程阻塞,等待其他線程完成給定任務(wù),比如,等待線程完成下載任務(wù)之后,提示用戶下載完成;導(dǎo)游等待所有游客參觀完之后去下一個(gè)景點(diǎn)等


咨詢
建站咨詢
