Java多線程通信問題深入瞭解
概述
多線程通信問題,也就是生產者與消費者問題
生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全
以下為百度百科對於該問題的解釋:
生產者與消費者問題:
生產者消費者問題(Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述瞭兩個共享固定大小緩沖區的線程——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩沖區中,然後重復此過程。與此同時,消費者也在緩沖區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩沖區滿時加入數據,消費者也不會在緩沖區中空時消耗數據。解決辦法:
要解決該問題,就必須讓生產者在緩沖區滿時休眠(要麼幹脆就放棄數據),等到下次消費者消耗緩沖區中的數據的時候,生產者才能被喚醒,開始往緩沖區添加數據。同樣,也可以讓消費者在緩沖區空時進入休眠,等到生產者往緩沖區添加數據之後,再喚醒消費者。通常采用進程間通信的方法解決該問題,常用的方法有信號燈法等。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。
引入
該過程可以類比為一個栗子:
廚師為生產者,服務員為消費者,假設隻有一個盤子盛放食品。
廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)後將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過後服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…
在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保瞭數據的安全
根據廚師和服務員這個栗子,我們可以通過代碼來一步步實現
- 定義廚師線程
/** * 廚師,是一個線程 */ static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f = f; } //運行的線程,生成100道菜 @Override public void run() { for (int i = 0 ; i < 100; i ++){ if(i % 2 == 0){ f.setNameAneTaste("小米粥","沒味道,不好吃"); }else{ f.setNameAneTaste("老北京雞肉卷","甜辣味"); } } } }
- 定義服務員線程
/** * 服務員,是一個線程 */ static class Waiter extends Thread{ private Food f; public Waiter(Food f){ this.f = f; } @Override public void run() { for(int i =0 ; i < 100;i ++){ //等待 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } }//end run }//end waiter
- 新建食物類
/** * 食物,對象 */ static class Food{ private String name; private String taste; public void setNameAneTaste(String name,String taste){ this.name = name; //加瞭這段之後,有可能這個地方的時間片更有可能被搶走,從而執行不瞭this.taste = taste try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; }//end set public void get(){ System.out.println("服務員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); } }//end food
main方法中去調用兩個線程
public static void main(String[] args) { Food f = new Food(); Cook c = new Cook(f); Waiter w = new Waiter(f); c.start();//廚師線程 w.start();//服務生線程 }
運行結果:
隻截取瞭一部分,我們可以看到,“小米粥”並沒有每次都對應“沒味道,不好吃”,“老北京雞肉卷”也沒有每次都對應“甜辣味”,而是一種錯亂的對應關系
…
服務員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
…
name和taste對應錯亂的原因:
當廚師調用set方法時,剛設置完name,程序進行瞭休眠,此時服務員可能已經將食品端走瞭,而此時的taste是上一次運行時保留的taste。
兩個線程一起運行時,由於使用搶占式調度模式,沒有協調,因此出現瞭該現象
以上運行結果解釋如圖:
加入線程安全
針對上面的線程不安全問題,對廚師set和服務員get這兩個線程都使用synchronized關鍵字,實現線程安全,即:當一個線程正在執行時,另外的線程不會執行,在後面排隊等待當前的程序執行完後再執行
代碼如下所示,分別給兩個方法添加synchronized修飾符,以方法為單位進行加鎖,實現線程安全
/** * 食物,對象 */ static class Food{ private String name; private String taste; public synchronized void setNameAneTaste(String name,String taste){ this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; }//end set public synchronized void get(){ System.out.println("服務員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); } }//end food
輸出結果:
由輸出可見,又出現瞭新的問題:
雖然加入瞭線程安全,set和get方法不再像前面一樣同時執行並且菜名和味道一一對應,但是set和get方法並沒有交替執行(通俗地講,不是廚師一做完服務員就端走),而是無序地執行(廚師有可能做完之後繼續做,做好幾道,服務員端好幾次…無規律地做和端)
…
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
…
實現生產者與消費者問題
由上面可知,加入線程安全依舊無法實現該問題。因此,要解決該問題,回到前面的引入部分,嚴格按照生產者與消費者問題中所說地去編寫程序
生產者與消費者問題:
生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全
↓
廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)後將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過後服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…
↓
在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保數據的安全
需要用到的java.lang.Object 中的方法:
變量和類型 | 方法 | 描述 |
---|---|---|
void | notify() | 喚醒當前this下的單個線程 |
void | notifyAll() | 喚醒當前this下的所有線程 |
void | wait() | 當前線程休眠 |
void | wait(long timeoutMillis) | 當前線程休眠一段時間 |
void | wait(long timeoutMillis, int nanos) | 當前線程休眠一段時間 |
- 首先在Food類中加一個標記flag:
True表示廚師生產,服務員休眠
False表示服務員端菜,廚師休眠
private boolean flag = true;
對set方法進行修改
當且僅當flag為True(True表示廚師生產,服務員休眠)時,才能進行做菜操作
做菜結束時,將flag置為False(False表示服務員端菜,廚師休眠),這樣廚師在生產完之後不會繼續生產,避免瞭廚師兩次生產、服務員端走一份的情況
然後喚醒在當前this下休眠的所有進程,而廚師線程進行休眠
public synchronized void setNameAneTaste(String name,String taste){ if(flag){//當標記為true時,表示廚師可以生產,該方法才執行 this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false;//生產完之後,標記置為false,這樣廚師在生產完之後不會繼續生產,避免瞭廚師兩次生產、服務員端走一份的情況 this.notifyAll();//喚醒在當前this下休眠的所有進程 try { this.wait();//此時廚師線程進行休眠 } catch (InterruptedException e) { e.printStackTrace(); } } }//end set
- 對get方法進行修改
當且僅當flag為False(False表示服務員端菜,廚師休眠)時,才能進行端菜操作
端菜結束時,將flag置為True(True表示廚師生產,服務員休眠),這樣服務員在端完菜之後不會繼續端菜,避免瞭服務員兩次端菜、廚師生產一份的情況
然後喚醒在當前this下休眠的所有進程,而服務員線程進行休眠
public synchronized void get(){ if(!flag){//廚師休眠的時候,服務員開始端菜 System.out.println("服務員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); flag = true;//端完之後,標記置為true,這樣服務員在端完菜之後不會繼續端菜,避免瞭服務員兩次端菜、廚師隻生產一份的情況 this.notifyAll();//喚醒在當前this下休眠的所有進程 try { this.wait();//此時服務員線程進行休眠 } catch (InterruptedException e) { e.printStackTrace(); } }// end if }//end get
作瞭以上調整之後的程序輸出:
我們可以看到,沒有出現數據錯亂,並且菜的順序是交替依次進行的
…
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
…
這就是生產者與消費者問題的一個典型例子
總結
本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- 分析java並發中的wait notify notifyAll
- Java多線程Thread類的使用及註意事項
- java wait()/notify() 實現生產者消費者模式詳解
- Java多線程之搞定最後一公裡詳解
- Java如何正確的使用wait-notify方法你知道嗎