您好,登錄后才能下訂單哦!
Java中怎么實(shí)現(xiàn)多線程通信,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
多線程通信問題,也就是生產(chǎn)者與消費(fèi)者問題
生產(chǎn)者和消費(fèi)者為兩個(gè)線程,兩個(gè)線程在運(yùn)行過程中交替睡眠,生產(chǎn)者在生產(chǎn)時(shí)消費(fèi)者沒有在消費(fèi),消費(fèi)者在消費(fèi)時(shí)生產(chǎn)者沒有在生產(chǎn),確保數(shù)據(jù)安全
以下為百度百科對(duì)于該問題的解釋:
生產(chǎn)者與消費(fèi)者問題:
生產(chǎn)者消費(fèi)者問題(Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個(gè)多線程同步問題的經(jīng)典案例。該問題描述了兩個(gè)共享固定大小緩沖區(qū)的線程——即所謂的“生產(chǎn)者”和“消費(fèi)者”——在實(shí)際運(yùn)行時(shí)會(huì)發(fā)生的問題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過程。與此同時(shí),消費(fèi)者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問題的關(guān)鍵就是要保證生產(chǎn)者不會(huì)在緩沖區(qū)滿時(shí)加入數(shù)據(jù),消費(fèi)者也不會(huì)在緩沖區(qū)中空時(shí)消耗數(shù)據(jù)。解決辦法:
要解決該問題,就必須讓生產(chǎn)者在緩沖區(qū)滿時(shí)休眠(要么干脆就放棄數(shù)據(jù)),等到下次消費(fèi)者消耗緩沖區(qū)中的數(shù)據(jù)的時(shí)候,生產(chǎn)者才能被喚醒,開始往緩沖區(qū)添加數(shù)據(jù)。同樣,也可以讓消費(fèi)者在緩沖區(qū)空時(shí)進(jìn)入休眠,等到生產(chǎn)者往緩沖區(qū)添加數(shù)據(jù)之后,再喚醒消費(fèi)者。通常采用進(jìn)程間通信的方法解決該問題,常用的方法有信號(hào)燈法等。如果解決方法不夠完善,則容易出現(xiàn)死鎖的情況。出現(xiàn)死鎖時(shí),兩個(gè)線程都會(huì)陷入休眠,等待對(duì)方喚醒自己。該問題也能被推廣到多個(gè)生產(chǎn)者和消費(fèi)者的情形。
該過程可以類比為一個(gè)栗子:
廚師為生產(chǎn)者,服務(wù)員為消費(fèi)者,假設(shè)只有一個(gè)盤子盛放食品。
廚師在生產(chǎn)食品(廚師線程運(yùn)行)的過程中,服務(wù)員應(yīng)當(dāng)?shù)却ǚ?wù)員線程睡眠),等到食品生產(chǎn)完成(廚師線程結(jié)束)后將食品放入盤子中,服務(wù)員將盤子端出去(服務(wù)員線程運(yùn)行),此時(shí)沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時(shí)間過后服務(wù)員將盤子拿回來(服務(wù)員線程結(jié)束),廚師開始進(jìn)行生產(chǎn)食品(廚師線程運(yùn)行),服務(wù)員在一旁等待(服務(wù)員線程睡眠)…
在此過程中,廚師和服務(wù)員兩個(gè)線程交替睡眠,廚師在做飯時(shí)服務(wù)員沒有端盤子(廚師線程運(yùn)行時(shí)服務(wù)員線程睡眠),服務(wù)員在端盤子時(shí)廚師沒有在做飯(服務(wù)員線程運(yùn)行時(shí)廚師線程睡眠),確保了數(shù)據(jù)的安全
根據(jù)廚師和服務(wù)員這個(gè)栗子,我們可以通過代碼來一步步實(shí)現(xiàn)
定義廚師線程
/** * 廚師,是一個(gè)線程 */ static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f = f; } //運(yùn)行的線程,生成100道菜 @Override public void run() { for (int i = 0 ; i < 100; i ++){ if(i % 2 == 0){ f.setNameAneTaste("小米粥","沒味道,不好吃"); }else{ f.setNameAneTaste("老北京雞肉卷","甜辣味"); } } } }
定義服務(wù)員線程
/** * 服務(wù)員,是一個(gè)線程 */ 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
新建食物類
/** * 食物,對(duì)象 */ static class Food{ private String name; private String taste; public void setNameAneTaste(String name,String taste){ this.name = name; //加了這段之后,有可能這個(gè)地方的時(shí)間片更有可能被搶走,從而執(zhí)行不了this.taste = taste try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; }//end set public void get(){ System.out.println("服務(wù)員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); } }//end food
main方法中去調(diào)用兩個(gè)線程
public static void main(String[] args) { Food f = new Food(); Cook c = new Cook(f); Waiter w = new Waiter(f); c.start();//廚師線程 w.start();//服務(wù)生線程 }
運(yùn)行結(jié)果:
只截取了一部分,我們可以看到,“小米粥”并沒有每次都對(duì)應(yīng)“沒味道,不好吃”,“老北京雞肉卷”也沒有每次都對(duì)應(yīng)“甜辣味”,而是一種錯(cuò)亂的對(duì)應(yīng)關(guān)系
...
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
name和taste對(duì)應(yīng)錯(cuò)亂的原因:
當(dāng)廚師調(diào)用set方法時(shí),剛設(shè)置完name,程序進(jìn)行了休眠,此時(shí)服務(wù)員可能已經(jīng)將食品端走了,而此時(shí)的taste是上一次運(yùn)行時(shí)保留的taste。
兩個(gè)線程一起運(yùn)行時(shí),由于使用搶占式調(diào)度模式,沒有協(xié)調(diào),因此出現(xiàn)了該現(xiàn)象
以上運(yùn)行結(jié)果解釋如圖:
針對(duì)上面的線程不安全問題,對(duì)廚師set和服務(wù)員get這兩個(gè)線程都使用synchronized關(guān)鍵字,實(shí)現(xiàn)線程安全,即:當(dāng)一個(gè)線程正在執(zhí)行時(shí),另外的線程不會(huì)執(zhí)行,在后面排隊(duì)等待當(dāng)前的程序執(zhí)行完后再執(zhí)行
代碼如下所示,分別給兩個(gè)方法添加synchronized修飾符,以方法為單位進(jìn)行加鎖,實(shí)現(xiàn)線程安全
/** * 食物,對(duì)象 */ 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("服務(wù)員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); } }//end food
輸出結(jié)果:
由輸出可見,又出現(xiàn)了新的問題:
雖然加入了線程安全,set和get方法不再像前面一樣同時(shí)執(zhí)行并且菜名和味道一一對(duì)應(yīng),但是set和get方法并沒有交替執(zhí)行(通俗地講,不是廚師一做完服務(wù)員就端走),而是無序地執(zhí)行(廚師有可能做完之后繼續(xù)做,做好幾道,服務(wù)員端好幾次…無規(guī)律地做和端)
...
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
由上面可知,加入線程安全依舊無法實(shí)現(xiàn)該問題。因此,要解決該問題,回到前面的引入部分,嚴(yán)格按照生產(chǎn)者與消費(fèi)者問題中所說地去編寫程序
生產(chǎn)者與消費(fèi)者問題:
生產(chǎn)者和消費(fèi)者為兩個(gè)線程,兩個(gè)線程在運(yùn)行過程中交替睡眠,生產(chǎn)者在生產(chǎn)時(shí)消費(fèi)者沒有在消費(fèi),消費(fèi)者在消費(fèi)時(shí)生產(chǎn)者沒有在生產(chǎn),確保數(shù)據(jù)安全
↓
廚師在生產(chǎn)食品(廚師線程運(yùn)行)的過程中,服務(wù)員應(yīng)當(dāng)?shù)却ǚ?wù)員線程睡眠),等到食品生產(chǎn)完成(廚師線程結(jié)束)后將食品放入盤子中,服務(wù)員將盤子端出去(服務(wù)員線程運(yùn)行),此時(shí)沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時(shí)間過后服務(wù)員將盤子拿回來(服務(wù)員線程結(jié)束),廚師開始進(jìn)行生產(chǎn)食品(廚師線程運(yùn)行),服務(wù)員在一旁等待(服務(wù)員線程睡眠)…
↓
在此過程中,廚師和服務(wù)員兩個(gè)線程交替睡眠,廚師在做飯時(shí)服務(wù)員沒有端盤子(廚師線程運(yùn)行時(shí)服務(wù)員線程睡眠),服務(wù)員在端盤子時(shí)廚師沒有在做飯(服務(wù)員線程運(yùn)行時(shí)廚師線程睡眠),確保數(shù)據(jù)的安全
需要用到的java.lang.Object 中的方法:
變量和類型 | 方法 | 描述 |
---|---|---|
void | notify() | 喚醒當(dāng)前this下的單個(gè)線程 |
void | notifyAll() | 喚醒當(dāng)前this下的所有線程 |
void | wait() | 當(dāng)前線程休眠 |
void | wait(long timeoutMillis) | 當(dāng)前線程休眠一段時(shí)間 |
void | wait(long timeoutMillis, int nanos) | 當(dāng)前線程休眠一段時(shí)間 |
首先在Food類中加一個(gè)標(biāo)記flag:
True表示廚師生產(chǎn),服務(wù)員休眠
False表示服務(wù)員端菜,廚師休眠
private boolean flag = true;
對(duì)set方法進(jìn)行修改
當(dāng)且僅當(dāng)flag為True(True表示廚師生產(chǎn),服務(wù)員休眠)時(shí),才能進(jìn)行做菜操作
做菜結(jié)束時(shí),將flag置為False(False表示服務(wù)員端菜,廚師休眠),這樣廚師在生產(chǎn)完之后不會(huì)繼續(xù)生產(chǎn),避免了廚師兩次生產(chǎn)、服務(wù)員端走一份的情況
然后喚醒在當(dāng)前this下休眠的所有進(jìn)程,而廚師線程進(jìn)行休眠
public synchronized void setNameAneTaste(String name,String taste){ if(flag){//當(dāng)標(biāo)記為true時(shí),表示廚師可以生產(chǎn),該方法才執(zhí)行 this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false;//生產(chǎn)完之后,標(biāo)記置為false,這樣廚師在生產(chǎn)完之后不會(huì)繼續(xù)生產(chǎn),避免了廚師兩次生產(chǎn)、服務(wù)員端走一份的情況 this.notifyAll();//喚醒在當(dāng)前this下休眠的所有進(jìn)程 try { this.wait();//此時(shí)廚師線程進(jìn)行休眠 } catch (InterruptedException e) { e.printStackTrace(); } } }//end set
對(duì)get方法進(jìn)行修改
當(dāng)且僅當(dāng)flag為False(False表示服務(wù)員端菜,廚師休眠)時(shí),才能進(jìn)行端菜操作
端菜結(jié)束時(shí),將flag置為True(True表示廚師生產(chǎn),服務(wù)員休眠),這樣服務(wù)員在端完菜之后不會(huì)繼續(xù)端菜,避免了服務(wù)員兩次端菜、廚師生產(chǎn)一份的情況
然后喚醒在當(dāng)前this下休眠的所有進(jìn)程,而服務(wù)員線程進(jìn)行休眠
public synchronized void get(){ if(!flag){//廚師休眠的時(shí)候,服務(wù)員開始端菜 System.out.println("服務(wù)員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); flag = true;//端完之后,標(biāo)記置為true,這樣服務(wù)員在端完菜之后不會(huì)繼續(xù)端菜,避免了服務(wù)員兩次端菜、廚師只生產(chǎn)一份的情況 this.notifyAll();//喚醒在當(dāng)前this下休眠的所有進(jìn)程 try { this.wait();//此時(shí)服務(wù)員線程進(jìn)行休眠 } catch (InterruptedException e) { e.printStackTrace(); } }// end if }//end get
看完上述內(nèi)容,你們掌握J(rèn)ava中怎么實(shí)現(xiàn)多線程通信的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。