線程通信,在多線程系統(tǒng)中,不同的線程執(zhí)行不同的任務(wù);如果這些任務(wù)之間存在聯(lián)系,那么執(zhí)行這些任務(wù)的線程之間就必須能夠通信,共同協(xié)調(diào)完成系統(tǒng)任務(wù)。
生產(chǎn)者、消費(fèi)者案例
案例分析
在案例中明,蔬菜基地作為生產(chǎn)者,負(fù)責(zé)生產(chǎn)蔬菜,并向超市輸送生產(chǎn)的蔬菜;消費(fèi)者通過向超市購買獲得蔬菜;超市怎作為生產(chǎn)者和消費(fèi)者之間的共享資源,都會和超市有聯(lián)系;蔬菜基地、共享資源、消費(fèi)者之間的交互流程如下:
在這個(gè)案例中,為什么不設(shè)計(jì)成生產(chǎn)者直接與給消費(fèi)者交互?讓兩者直接交換數(shù)據(jù)不是更好嗎,選擇先先把數(shù)據(jù)存儲到共享資源中,然后消費(fèi)者再從共享資源中取出數(shù)據(jù)使用,中間多了一個(gè)環(huán)節(jié)不是更麻煩了?
其實(shí)不是的,設(shè)計(jì)成這樣是有原因的,因?yàn)檫@樣設(shè)計(jì)很好的體現(xiàn)了面向?qū)ο蟮?strong>低耦合的設(shè)計(jì)理念;通過這樣實(shí)現(xiàn)的程序能更加符合人的操作理念,更加貼合現(xiàn)實(shí)環(huán)境;同時(shí),也能很好的避免因生產(chǎn)者與消費(fèi)者直接交互而導(dǎo)致的操作不安全的問題。
我們來對高耦合和低耦合做一個(gè)對比就會很直觀了:
高(緊)耦合:生產(chǎn)者與消費(fèi)者直接交互,生產(chǎn)者(蔬菜基地)把蔬菜直接給到給消費(fèi)者,雙方之間的依賴程度很高;此時(shí),生產(chǎn)者中就必須持有消費(fèi)者對象的引用,同樣的道理,消費(fèi)者也必須要持有生產(chǎn)者對象的引用;這樣,消費(fèi)者和生產(chǎn)者才能夠直接交互。
低(松)耦合:引入一個(gè)中間對象(共享資源)來,將生產(chǎn)者、消費(fèi)者中需要對外輸出或者從外數(shù)據(jù)的操作封裝到中間對象中,這樣,消費(fèi)者和生產(chǎn)者將會持有這個(gè)中間對象的引用,屏蔽了生產(chǎn)者和消費(fèi)者直接的數(shù)據(jù)交互.,大大見減小生產(chǎn)者和消費(fèi)者之間的依賴程度。
關(guān)于高耦合和低耦合的區(qū)別,電腦中主機(jī)中的集成顯卡和獨(dú)立顯卡也是一個(gè)非常好的例子。
集成顯卡普遍都集成于CPU中,所以如果集成顯卡出現(xiàn)了問題需要更換,那么會連著CPU一塊更換,其維護(hù)成本與CPU其實(shí)是一樣的;
獨(dú)立顯卡需要插在主板的顯卡接口上才能與計(jì)算機(jī)通信,其相對于整個(gè)計(jì)算機(jī)系統(tǒng)來說,是獨(dú)立的存在,即便出現(xiàn)問題需要更換,也只更換顯卡即可。
案例的代碼實(shí)現(xiàn)
接下來我們使用多線程技術(shù)實(shí)現(xiàn)該案例,案例代碼如下:
蔬菜基地對象,VegetableBase.java
// VegetableBase.java
// 蔬菜基地
public class VegetableBase implements Runnable {
// 超市實(shí)例
private Supermarket supermarket = null;
public VegetableBase(Supermarket supermarket) {
this.supermarket = supermarket;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
supermarket.push("黃瓜", 1300);
System.out.println("push : 黃瓜 " + 1300);
} else {
supermarket.push("青菜", 1400);
System.out.println("push : 青菜 " + 1400);
}
}
}
}
消費(fèi)者對象,Consumer.java
// Consumer.java
// 消費(fèi)者
public class Consumer implements Runnable {
// 超市實(shí)例
private Supermarket supermarket = null;
public Consumer(Supermarket supermarket) {
this.supermarket = supermarket;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
supermarket.popup();
}
}
}
超市對象,Supermarket.java
// Supermarket.java
// 超市
public class Supermarket {
// 蔬菜名稱
private String name;
// 蔬菜數(shù)量
private Integer num;
// 蔬菜基地想超市輸送蔬菜
public void push(String name, Integer num) {
this.name = name;
this.num = num;
}
// 用戶從超市中購買蔬菜
public void popup() {
// 為了讓效果更明顯,在這里模擬網(wǎng)絡(luò)延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
}
}
運(yùn)行案例,App.java
// 案例應(yīng)用入口
public class App {
public static void main(String[] args) {
// 創(chuàng)建超市實(shí)例
Supermarket supermarket = new Supermarket();
// 蔬菜基地線程啟動, 開始往超市輸送蔬菜
new Thread(new VegetableBase(supermarket)).start();
new Thread(new VegetableBase(supermarket)).start();
// 消費(fèi)者線程啟動,消費(fèi)者開始購買蔬菜
new Thread(new Consumer(supermarket)).start();
new Thread(new Consumer(supermarket)).start();
}
}
發(fā)現(xiàn)了問題
運(yùn)行該案例,打印出運(yùn)行結(jié)果,外表一片祥和,可還是被敏銳的發(fā)現(xiàn)了問題,問題如下所示:
在一片看似祥和的打印結(jié)果中,出現(xiàn)了一個(gè)很不祥和的特例,生產(chǎn)基地在輸送蔬菜時(shí),黃瓜的數(shù)量一直都是1300顆,青菜的數(shù)量一直是1400顆,但是在消費(fèi)者消費(fèi)時(shí)卻出現(xiàn)了蔬菜名稱是黃瓜的,但數(shù)量卻是青菜的數(shù)量的情況。
之所以出現(xiàn)這樣的問題,是因?yàn)樵诒景咐蚕淼馁Y源中,多個(gè)線程共同競爭資源時(shí)沒有使用同步操作,而是異步操作,今兒導(dǎo)致了資源分配紊亂的情況;需要注意的是,并不是因?yàn)槲覀冊诎咐惺褂?code>Thread.sleep();模擬網(wǎng)絡(luò)延遲才導(dǎo)致問題出現(xiàn),而是本來就存在問題,使用Thread.sleep();只是讓問題更加明顯。
案例問題的解決
在本案例中需要解決的問題有兩個(gè),分別如下:
- 問題一:蔬菜名稱和數(shù)量不匹配的問題。
- 問題二:需要保證超市無貨時(shí)生產(chǎn),超市有貨時(shí)才消費(fèi)。
針對問題一解決方案:保證蔬菜基地在輸送蔬菜的過程保持同步,中間不能被其他線程(特別是消費(fèi)者線程)干擾,打亂輸送操作;直至當(dāng)前線程完成輸送后,其他線程才能進(jìn)入操作,同樣的,當(dāng)有線程進(jìn)入操作后,其他線程只能在操作外等待。
所以,技術(shù)方案可以使用同步代碼塊/同步方法/Lock機(jī)制來保持操作的同步性。
針對問題二的解決方案:給超市一個(gè)有無貨的狀態(tài)標(biāo)志,
超市無貨時(shí),蔬菜基地輸送蔬菜補(bǔ)貨,此時(shí)生產(chǎn)基地線程可操作;
超市有貨時(shí),消費(fèi)者線程可操作;就是:
保證生產(chǎn)基地 ——> 共享資源 ——> 消費(fèi)者這個(gè)整個(gè)流程的完整運(yùn)行。
技術(shù)方案:使用線程中的等待和喚醒機(jī)制。
同步操作,分為同步代碼塊和同步方法兩種。詳情可查看我的另外一篇關(guān)于多線程的文章:「JAVA」Java 線程不安全分析,同步鎖和Lock機(jī)制,哪個(gè)解決方案更好
- 在同步代碼塊中的同步鎖必須選擇多個(gè)線程共同的資源對象,當(dāng)前生產(chǎn)者線程在生產(chǎn)數(shù)據(jù)的時(shí)候(先擁有同步鎖),其他線程就在鎖池中等待獲取鎖;當(dāng)生產(chǎn)者線程執(zhí)行完同步代碼塊的時(shí)候,就會釋放同步鎖,其他線程開始搶鎖的使用權(quán),搶到后就會擁有該同步鎖,執(zhí)行完成后釋放,其他線程再開始搶鎖的使用權(quán),依次往復(fù)執(zhí)行。
- 多個(gè)線程只有使用同一個(gè)對象(就好比案例中的共享資源對象)的時(shí)候,多線程之間才有互斥效果,我們把這個(gè)用來做互斥的對象稱之為同步監(jiān)聽對象,又稱同步監(jiān)聽器、互斥鎖、同步鎖,同步鎖是一個(gè)抽象概念,可以理解為在對象上標(biāo)記了一把鎖。
- 同步鎖對象可以選擇任意類型的對象即可,只需要保證多個(gè)線程使用的是相同鎖對象即可。在任何時(shí)候,最多只能運(yùn)行一個(gè)線程擁有同步鎖。因?yàn)橹挥型奖O(jiān)聽鎖對象才能調(diào)用
wait和notify方法,wait和notify方法存在于Object類中。
線程通信之 wait和notify方法
在java.lang.Object類中提供了用于操作線程通信的方法,詳情如下:
-
wait():執(zhí)行該方法的線程對象會釋放同步鎖,然后JVM把該線程存放到等待池中,等待著其他線程來喚醒該線程; -
notify():執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的的任意一個(gè)線程,把線程轉(zhuǎn)到同步鎖池中等待; -
notifyAll():執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的所有的線程,把這些線程轉(zhuǎn)到同步鎖池中等待;
注意:上述方法只能被同步監(jiān)聽鎖對象來調(diào)用,否則發(fā)生 IllegalMonitorStateException。
wait和notify方法應(yīng)用實(shí)例
假設(shè)A線程和B線程共同操作一個(gè)X對象(同步鎖),A、B線程可以通過X對象的wait和notify方法來進(jìn)行通信,流程如下:
- 當(dāng)
A線程執(zhí)行X對象的同步方法時(shí),A線程持有X對象的鎖,B線程沒有執(zhí)行機(jī)會,此時(shí)的B線程會在X對象的鎖池中等待; - 當(dāng)
A線程在同步方法中執(zhí)行X.wait()方法時(shí),A線程會釋放X對象的同步鎖,然后進(jìn)入X對象的等待池中; - 接著,在
X對象的鎖池中等待鎖的B線程獲取X對象的鎖,執(zhí)行X的另一個(gè)同步方法; - 當(dāng)
B線程在同步方法中執(zhí)行X.notify()方法時(shí),JVM會把A線程從X對象的等待池中轉(zhuǎn)到X對象的同步鎖池中,等待獲取鎖的使用權(quán); - 當(dāng)
B線程執(zhí)行完同步方法后,會釋放擁有的鎖,然后A線程獲得鎖,繼續(xù)執(zhí)行同步方法;
基于上述機(jī)制,我們就可以使用同步操作 + wait和notify方法來解決案例中的問題了,重新來實(shí)現(xiàn)共享資源——超市對象:
// 超市
public class Supermarket {
// 蔬菜名稱
private String name;
// 蔬菜數(shù)量
private Integer num;
// 超市是否為空
private Boolean isEmpty = true;
// 蔬菜基地向超市輸送蔬菜
public synchronized void push(String name, Integer num) {
try {
while (!isEmpty) { // 超市有貨時(shí),不再輸送蔬菜,而是要等待消費(fèi)者獲取
this.wait();
}
this.name = name;
this.num = num;
isEmpty = false;
this.notify(); // 喚醒另一個(gè)線程
} catch(Exception e) {
}
}
// 用戶從超市中購買蔬菜
public synchronized void popup() {
try {
while (isEmpty) { // 超市無貨時(shí),不再提供消費(fèi),而是要等待蔬菜基地輸送
this.wait();
}
// 為了讓效果更明顯,在這里模擬網(wǎng)絡(luò)延遲
Thread.sleep(1000);
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
isEmpty = true;
this.notify(); // 喚醒另一線程
} catch (Exception e) {
}
}
}
線程通信之 使用Lock和Condition接口
由于wait和notify方法,只能被同步監(jiān)聽鎖對象來調(diào)用,否則發(fā)生
IllegalMonitorStateException。從Java 5開始,提供了Lock機(jī)制,同時(shí)還有處理Lock機(jī)制的通信控制的Condition接口。Lock機(jī)制沒有同步鎖的概念,也就沒有自動獲取鎖和自動釋放鎖的這樣的操作了。
因?yàn)闆]有同步鎖,所以Lock機(jī)制中的線程通信就不能調(diào)用wait和notify方法了;同樣的,Java 5 中也提供了解決方案,因此從Java 5開始,可以:
- 使用
Lock機(jī)制取代synchronized代碼塊和synchronized方法; - 使用
Condition接口對象的await、signal、signalAll方法取代Object類中的wait、notify、notifyAll方法;
Lock和Condition接口的性能也比同步操作要高很多,所以這種方式也是我們推薦使用的方式。
我們可以使用Lock機(jī)制和Condition接口 方法來解決案例中的問題,重新來實(shí)現(xiàn)的共享資源——超市對象,代碼如下:
// 超市
public class Supermarket {
// 蔬菜名稱
private String name;
// 蔬菜數(shù)量
private Integer num;
// 超市是否為空
private Boolean isEmpty = true;
// lock
private final Lock lock = new ReentrantLock();
// Condition
private Condition condition = lock.newCondition();
// 蔬菜基地向超市輸送蔬菜
public synchronized void push(String name, Integer num) {
lock.lock(); // 獲取鎖
try {
while (!isEmpty) { // 超市有貨時(shí),不再輸送蔬菜,而是要等待消費(fèi)者獲取
condition.await();
}
this.name = name;
this.num = num;
isEmpty = false;
condition.signalAll();
} catch(Exception e) {
} finally {
lock.unlock(); // 釋放鎖
}
}
// 用戶從超市中購買蔬菜
public synchronized void popup() {
lock.lock();
try {
while (isEmpty) { // 超市無貨時(shí),不再提供消費(fèi),而是要等待蔬菜基地輸送
condition.await();
}
// 為了讓效果更明顯,在這里模擬網(wǎng)絡(luò)延遲
Thread.sleep(1000);
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
isEmpty = true;
condition.signalAll();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
完結(jié),老夫雖不正經(jīng),但老夫一身的才華!關(guān)注我,獲取更多編程科技知識。