線程通信,在多線程系統(tǒng)中拱撵,不同的線程執(zhí)行不同的任務辉川;如果這些任務之間存在聯(lián)系,那么執(zhí)行這些任務的線程之間就必須能夠通信拴测,共同協(xié)調完成系統(tǒng)任務乓旗。
生產(chǎn)者、消費者案例
案例分析
在案例中明集索,蔬菜基地作為生產(chǎn)者屿愚,負責生產(chǎn)蔬菜汇跨,并向超市輸送生產(chǎn)的蔬菜;消費者通過向超市購買獲得蔬菜妆距;超市怎作為生產(chǎn)者和消費者之間的共享資源穷遂,都會和超市有聯(lián)系;蔬菜基地娱据、共享資源蚪黑、消費者之間的交互流程如下:
在這個案例中吸耿,為什么不設計成生產(chǎn)者直接與給消費者交互祠锣?讓兩者直接交換數(shù)據(jù)不是更好嗎,選擇先先把數(shù)據(jù)存儲到共享資源中咽安,然后消費者再從共享資源中取出數(shù)據(jù)使用伴网,中間多了一個環(huán)節(jié)不是更麻煩了?
其實不是的妆棒,設計成這樣是有原因的澡腾,因為這樣設計很好的體現(xiàn)了面向對象的低耦合的設計理念;通過這樣實現(xiàn)的程序能更加符合人的操作理念糕珊,更加貼合現(xiàn)實環(huán)境动分;同時,也能很好的避免因生產(chǎn)者與消費者直接交互而導致的操作不安全的問題红选。
我們來對高耦合和低耦合做一個對比就會很直觀了:
高(緊)耦合:生產(chǎn)者與消費者直接交互澜公,生產(chǎn)者(蔬菜基地)把蔬菜直接給到給消費者,雙方之間的依賴程度很高喇肋;此時坟乾,生產(chǎn)者中就必須持有消費者對象的引用,同樣的道理蝶防,消費者也必須要持有生產(chǎn)者對象的引用甚侣;這樣,消費者和生產(chǎn)者才能夠直接交互间学。
低(松)耦合:引入一個中間對象(共享資源)來殷费,將生產(chǎn)者、消費者中需要對外輸出或者從外數(shù)據(jù)的操作封裝到中間對象中低葫,這樣详羡,消費者和生產(chǎn)者將會持有這個中間對象的引用,屏蔽了生產(chǎn)者和消費者直接的數(shù)據(jù)交互.氮采,大大見減小生產(chǎn)者和消費者之間的依賴程度殷绍。
關于高耦合和低耦合的區(qū)別,電腦中主機中的集成顯卡和獨立顯卡也是一個非常好的例子鹊漠。
集成顯卡普遍都集成于CPU中主到,所以如果集成顯卡出現(xiàn)了問題需要更換,那么會連著CPU一塊更換躯概,其維護成本與CPU其實是一樣的登钥;
獨立顯卡需要插在主板的顯卡接口上才能與計算機通信,其相對于整個計算機系統(tǒng)來說娶靡,是獨立的存在牧牢,即便出現(xiàn)問題需要更換,也只更換顯卡即可姿锭。
案例的代碼實現(xiàn)
接下來我們使用多線程技術實現(xiàn)該案例塔鳍,案例代碼如下:
蔬菜基地對象,VegetableBase.java
// VegetableBase.java
// 蔬菜基地
public class VegetableBase implements Runnable {
// 超市實例
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);
}
}
}
}
消費者對象呻此,Consumer.java
// Consumer.java
// 消費者
public class Consumer implements Runnable {
// 超市實例
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)絡延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆焚鲜。");
}
}
運行案例掌唾,App.java
// 案例應用入口
public class App {
public static void main(String[] args) {
// 創(chuàng)建超市實例
Supermarket supermarket = new Supermarket();
// 蔬菜基地線程啟動, 開始往超市輸送蔬菜
new Thread(new VegetableBase(supermarket)).start();
new Thread(new VegetableBase(supermarket)).start();
// 消費者線程啟動,消費者開始購買蔬菜
new Thread(new Consumer(supermarket)).start();
new Thread(new Consumer(supermarket)).start();
}
}
發(fā)現(xiàn)了問題
運行該案例忿磅,打印出運行結果糯彬,外表一片祥和,可還是被敏銳的發(fā)現(xiàn)了問題葱她,問題如下所示:
在一片看似祥和的打印結果中撩扒,出現(xiàn)了一個很不祥和的特例,生產(chǎn)基地在輸送蔬菜時吨些,黃瓜的數(shù)量一直都是1300
顆搓谆,青菜的數(shù)量一直是1400
顆,但是在消費者消費時卻出現(xiàn)了蔬菜名稱是黃瓜的锤灿,但數(shù)量卻是青菜的數(shù)量的情況挽拔。
之所以出現(xiàn)這樣的問題,是因為在本案例共享的資源中但校,多個線程共同競爭資源時沒有使用同步操作螃诅,而是異步操作,今兒導致了資源分配紊亂的情況状囱;需要注意的是术裸,并不是因為我們在案例中使用Thread.sleep();
模擬網(wǎng)絡延遲才導致問題出現(xiàn),而是本來就存在問題亭枷,使用Thread.sleep();
只是讓問題更加明顯袭艺。
案例問題的解決
在本案例中需要解決的問題有兩個,分別如下:
- 問題一:蔬菜名稱和數(shù)量不匹配的問題叨粘。
- 問題二:需要保證超市無貨時生產(chǎn)猾编,超市有貨時才消費瘤睹。
針對問題一解決方案:保證蔬菜基地在輸送蔬菜的過程保持同步,中間不能被其他線程(特別是消費者線程)干擾答倡,打亂輸送操作轰传;直至當前線程完成輸送后,其他線程才能進入操作瘪撇,同樣的获茬,當有線程進入操作后,其他線程只能在操作外等待倔既。
所以恕曲,技術方案可以使用同步代碼塊/同步方法/Lock機制來保持操作的同步性。
針對問題二的解決方案:給超市一個有無貨的狀態(tài)標志渤涌,
超市無貨時佩谣,蔬菜基地輸送蔬菜補貨,此時生產(chǎn)基地線程可操作歼捏;
超市有貨時稿存,消費者線程可操作;就是:
保證生產(chǎn)基地 ——> 共享資源 ——> 消費者
這個整個流程的完整運行瞳秽。
技術方案:使用線程中的等待和喚醒機制瓣履。
同步操作,分為同步代碼塊和同步方法兩種练俐。詳情可查看我的另外一篇關于多線程的文章:「JAVA」Java 線程不安全分析袖迎,同步鎖和Lock機制,哪個解決方案更好
- 在同步代碼塊中的同步鎖必須選擇多個線程共同的資源對象腺晾,當前生產(chǎn)者線程在生產(chǎn)數(shù)據(jù)的時候(先擁有同步鎖)燕锥,其他線程就在鎖池中等待獲取鎖;當生產(chǎn)者線程執(zhí)行完同步代碼塊的時候悯蝉,就會釋放同步鎖归形,其他線程開始搶鎖的使用權,搶到后就會擁有該同步鎖鼻由,執(zhí)行完成后釋放暇榴,其他線程再開始搶鎖的使用權,依次往復執(zhí)行蕉世。
- 多個線程只有使用同一個對象(就好比案例中的共享資源對象)的時候蔼紧,多線程之間才有互斥效果,我們把這個用來做互斥的對象稱之為同步監(jiān)聽對象狠轻,又稱同步監(jiān)聽器奸例、互斥鎖、同步鎖向楼,同步鎖是一個抽象概念查吊,可以理解為在對象上標記了一把鎖谐区。
- 同步鎖對象可以選擇任意類型的對象即可,只需要保證多個線程使用的是相同鎖對象即可菩貌。在任何時候卢佣,最多只能運行一個線程擁有同步鎖重荠。因為只有同步監(jiān)聽鎖對象才能調用
wait
和notify
方法箭阶,wait
和notify
方法存在于Object
類中。
線程通信之 wait和notify方法
在java.lang.Object
類中提供了用于操作線程通信的方法戈鲁,詳情如下:
-
wait()
:執(zhí)行該方法的線程對象會釋放同步鎖仇参,然后JVM
把該線程存放到等待池中,等待著其他線程來喚醒該線程婆殿; -
notify()
:執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的的任意一個線程诈乒,把線程轉到同步鎖池中等待; -
notifyAll()
:執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的所有的線程婆芦,把這些線程轉到同步鎖池中等待怕磨;
注意:上述方法只能被同步監(jiān)聽鎖對象來調用,否則發(fā)生 IllegalMonitorStateException
消约。
wait和notify方法應用實例
假設A線程
和B線程
共同操作一個X對象
(同步鎖)肠鲫,A、B線程
可以通過X對象
的wait
和notify
方法來進行通信或粮,流程如下:
- 當
A線程
執(zhí)行X對象
的同步方法時导饲,A線程
持有X對象
的鎖,B線程
沒有執(zhí)行機會氯材,此時的B線程
會在X對象
的鎖池中等待渣锦; - 當
A線程
在同步方法中執(zhí)行X.wait()
方法時,A線程
會釋放X對象
的同步鎖氢哮,然后進入X對象
的等待池中袋毙; - 接著,在
X對象
的鎖池中等待鎖的B線程
獲取X對象
的鎖冗尤,執(zhí)行X
的另一個同步方法听盖; - 當
B線程
在同步方法中執(zhí)行X.notify()
方法時,JVM
會把A線程
從X對象
的等待池中轉到X對象
的同步鎖池中生闲,等待獲取鎖的使用權媳溺; - 當
B線程
執(zhí)行完同步方法后,會釋放擁有的鎖碍讯,然后A線程
獲得鎖悬蔽,繼續(xù)執(zhí)行同步方法;
基于上述機制捉兴,我們就可以使用同步操作 + wait和notify方法來解決案例中的問題了蝎困,重新來實現(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) { // 超市有貨時录语,不再輸送蔬菜,而是要等待消費者獲取
this.wait();
}
this.name = name;
this.num = num;
isEmpty = false;
this.notify(); // 喚醒另一個線程
} catch(Exception e) {
}
}
// 用戶從超市中購買蔬菜
public synchronized void popup() {
try {
while (isEmpty) { // 超市無貨時禾乘,不再提供消費澎埠,而是要等待蔬菜基地輸送
this.wait();
}
// 為了讓效果更明顯,在這里模擬網(wǎng)絡延遲
Thread.sleep(1000);
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆始藕。");
isEmpty = true;
this.notify(); // 喚醒另一線程
} catch (Exception e) {
}
}
}
線程通信之 使用Lock和Condition接口
由于wait
和notify
方法蒲稳,只能被同步監(jiān)聽鎖對象來調用,否則發(fā)生
IllegalMonitorStateException
伍派。從Java 5
開始江耀,提供了Lock機制
,同時還有處理Lock機制
的通信控制的Condition接口
诉植。Lock機制
沒有同步鎖的概念祥国,也就沒有自動獲取鎖和自動釋放鎖的這樣的操作了。
因為沒有同步鎖晾腔,所以Lock機制
中的線程通信就不能調用wait
和notify
方法了舌稀;同樣的,Java 5
中也提供了解決方案灼擂,因此從Java 5
開始壁查,可以:
- 使用
Lock機制
取代synchronized
代碼塊和synchronized
方法; - 使用
Condition接口
對象的await缤至、signal潮罪、signalAll
方法取代Object
類中的wait、notify领斥、notifyAll
方法嫉到;
Lock和Condition接口
的性能也比同步操作要高很多,所以這種方式也是我們推薦使用的方式月洛。
我們可以使用Lock機制和Condition接口
方法來解決案例中的問題何恶,重新來實現(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) { // 超市有貨時嚼黔,不再輸送蔬菜细层,而是要等待消費者獲取
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) { // 超市無貨時,不再提供消費唬涧,而是要等待蔬菜基地輸送
condition.await();
}
// 為了讓效果更明顯疫赎,在這里模擬網(wǎng)絡延遲
Thread.sleep(1000);
System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
isEmpty = true;
condition.signalAll();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
完結碎节,老夫雖不正經(jīng)捧搞,但老夫一身的才華!關注我,獲取更多編程科技知識胎撇。