Java多線程通信問題深入了解
概述
多線程通信問題,也就是生產者與消費者問題
生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全
以下為百度百科對于該問題的解釋:
生產者與消費者問題:
生產者消費者問題(Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩沖區(qū)的線程——即所謂的“生產者”和“消費者”——在實際運行時會發(fā)生的問題。生產者的主要作用是生成一定量的數據放到緩沖區(qū)中,然后重復此過程。與此同時,消費者也在緩沖區(qū)消耗這些數據。該問題的關鍵就是要保證生產者不會在緩沖區(qū)滿時加入數據,消費者也不會在緩沖區(qū)中空時消耗數據。解決辦法:
要解決該問題,就必須讓生產者在緩沖區(qū)滿時休眠(要么干脆就放棄數據),等到下次消費者消耗緩沖區(qū)中的數據的時候,生產者才能被喚醒,開始往緩沖區(qū)添加數據。同樣,也可以讓消費者在緩沖區(qū)空時進入休眠,等到生產者往緩沖區(qū)添加數據之后,再喚醒消費者。通常采用進程間通信的方法解決該問題,常用的方法有信號燈法等。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。
引入
該過程可以類比為一個栗子:
廚師為生產者,服務員為消費者,假設只有一個盤子盛放食品。
廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)后將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過后服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…
在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保了數據的安全
根據廚師和服務員這個栗子,我們可以通過代碼來一步步實現
- 定義廚師線程
/** * 廚師,是一個線程 */ 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; //加了這段之后,有可能這個地方的時間片更有可能被搶走,從而執(zhí)行不了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關鍵字,實現線程安全,即:當一個線程正在執(zhí)行時,另外的線程不會執(zhí)行,在后面排隊等待當前的程序執(zhí)行完后再執(zhí)行
代碼如下所示,分別給兩個方法添加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方法不再像前面一樣同時執(zhí)行并且菜名和味道一一對應,但是set和get方法并沒有交替執(zhí)行(通俗地講,不是廚師一做完服務員就端走),而是無序地執(zhí)行(廚師有可能做完之后繼續(xù)做,做好幾道,服務員端好幾次…無規(guī)律地做和端)
...
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
實現生產者與消費者問題
由上面可知,加入線程安全依舊無法實現該問題。因此,要解決該問題,回到前面的引入部分,嚴格按照生產者與消費者問題中所說地去編寫程序
生產者與消費者問題:
生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全
↓
廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)后將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過后服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…
↓
在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保數據的安全
需要用到的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表示服務員端菜,廚師休眠),這樣廚師在生產完之后不會繼續(xù)生產,避免了廚師兩次生產、服務員端走一份的情況
然后喚醒在當前this下休眠的所有進程,而廚師線程進行休眠
public synchronized void setNameAneTaste(String name,String taste){ if(flag){//當標記為true時,表示廚師可以生產,該方法才執(zhí)行 this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false;//生產完之后,標記置為false,這樣廚師在生產完之后不會繼續(xù)生產,避免了廚師兩次生產、服務員端走一份的情況 this.notifyAll();//喚醒在當前this下休眠的所有進程 try { this.wait();//此時廚師線程進行休眠 } catch (InterruptedException e) { e.printStackTrace(); } } }//end set
- 對get方法進行修改
當且僅當flag為False(False表示服務員端菜,廚師休眠)時,才能進行端菜操作
端菜結束時,將flag置為True(True表示廚師生產,服務員休眠),這樣服務員在端完菜之后不會繼續(xù)端菜,避免了服務員兩次端菜、廚師生產一份的情況
然后喚醒在當前this下休眠的所有進程,而服務員線程進行休眠
public synchronized void get(){ if(!flag){//廚師休眠的時候,服務員開始端菜 System.out.println("服務員端走的菜的名稱是:" + this.name + " 味道:" + this.taste); flag = true;//端完之后,標記置為true,這樣服務員在端完菜之后不會繼續(xù)端菜,避免了服務員兩次端菜、廚師只生產一份的情況 this.notifyAll();//喚醒在當前this下休眠的所有進程 try { this.wait();//此時服務員線程進行休眠 } catch (InterruptedException e) { e.printStackTrace(); } }// end if }//end get
作了以上調整之后的程序輸出:
我們可以看到,沒有出現數據錯亂,并且菜的順序是交替依次進行的
...
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
這就是生產者與消費者問題的一個典型例子
總結
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關注腳本之家的更多內容!
相關文章
SpringBoot實現定時發(fā)送郵件的三種方法案例詳解
這篇文章主要介紹了SpringBoot三種方法實現定時發(fā)送郵件的案例,Spring框架的定時任務調度功能支持配置和注解兩種方式Spring?Boot在Spring框架的基礎上實現了繼承,并對其中基于注解方式的定時任務實現了非常好的支持,本文給大家詳細講解,需要的朋友可以參考下2023-03-03Java8新特性之重復注解(repeating annotations)淺析
這篇文章主要介紹了Java8新特性之重復注解(repeating annotations)淺析,這個新特性只是修改了程序的可讀性,是比較小的一個改動,需要的朋友可以參考下2014-06-06IDEA報java:?java.lang.OutOfMemoryError:?Java?heap?space錯誤
這篇文章主要給大家介紹了關于IDEA報java:?java.lang.OutOfMemoryError:?Java?heap?space錯誤的解決辦法,文中將解決的辦法介紹的非常詳細,需要的朋友可以參考下2024-01-01Spring中TransactionSynchronizationManager的使用詳解
這篇文章主要介紹了Spring中TransactionSynchronizationManager的使用詳解,TransactionSynchronizationManager是事務同步管理器,監(jiān)聽事務的操作,來實現在事務前后可以添加一些指定操作,需要的朋友可以參考下2023-09-09