Java多線程通信問(wèn)題深入了解
概述
多線程通信問(wèn)題,也就是生產(chǎn)者與消費(fèi)者問(wèn)題
生產(chǎn)者和消費(fèi)者為兩個(gè)線程,兩個(gè)線程在運(yùn)行過(guò)程中交替睡眠,生產(chǎn)者在生產(chǎn)時(shí)消費(fèi)者沒(méi)有在消費(fèi),消費(fèi)者在消費(fèi)時(shí)生產(chǎn)者沒(méi)有在生產(chǎn),確保數(shù)據(jù)安全
以下為百度百科對(duì)于該問(wèn)題的解釋:
生產(chǎn)者與消費(fèi)者問(wèn)題:
生產(chǎn)者消費(fèi)者問(wèn)題(Producer-consumer problem),也稱有限緩沖問(wèn)題(Bounded-buffer problem),是一個(gè)多線程同步問(wèn)題的經(jīng)典案例。該問(wèn)題描述了兩個(gè)共享固定大小緩沖區(qū)的線程——即所謂的“生產(chǎn)者”和“消費(fèi)者”——在實(shí)際運(yùn)行時(shí)會(huì)發(fā)生的問(wèn)題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過(guò)程。與此同時(shí),消費(fèi)者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問(wèn)題的關(guān)鍵就是要保證生產(chǎn)者不會(huì)在緩沖區(qū)滿時(shí)加入數(shù)據(jù),消費(fèi)者也不會(huì)在緩沖區(qū)中空時(shí)消耗數(shù)據(jù)。解決辦法:
要解決該問(wèn)題,就必須讓生產(chǎn)者在緩沖區(qū)滿時(shí)休眠(要么干脆就放棄數(shù)據(jù)),等到下次消費(fèi)者消耗緩沖區(qū)中的數(shù)據(jù)的時(shí)候,生產(chǎn)者才能被喚醒,開(kāi)始往緩沖區(qū)添加數(shù)據(jù)。同樣,也可以讓消費(fèi)者在緩沖區(qū)空時(shí)進(jìn)入休眠,等到生產(chǎn)者往緩沖區(qū)添加數(shù)據(jù)之后,再喚醒消費(fèi)者。通常采用進(jìn)程間通信的方法解決該問(wèn)題,常用的方法有信號(hào)燈法等。如果解決方法不夠完善,則容易出現(xiàn)死鎖的情況。出現(xiàn)死鎖時(shí),兩個(gè)線程都會(huì)陷入休眠,等待對(duì)方喚醒自己。該問(wèn)題也能被推廣到多個(gè)生產(chǎn)者和消費(fèi)者的情形。
引入
該過(guò)程可以類比為一個(gè)栗子:
廚師為生產(chǎn)者,服務(wù)員為消費(fèi)者,假設(shè)只有一個(gè)盤(pán)子盛放食品。
廚師在生產(chǎn)食品(廚師線程運(yùn)行)的過(guò)程中,服務(wù)員應(yīng)當(dāng)?shù)却ǚ?wù)員線程睡眠),等到食品生產(chǎn)完成(廚師線程結(jié)束)后將食品放入盤(pán)子中,服務(wù)員將盤(pán)子端出去(服務(wù)員線程運(yùn)行),此時(shí)沒(méi)有盤(pán)子可以放食品,因此廚師休息(廚師線程休眠),一段時(shí)間過(guò)后服務(wù)員將盤(pán)子拿回來(lái)(服務(wù)員線程結(jié)束),廚師開(kāi)始進(jìn)行生產(chǎn)食品(廚師線程運(yùn)行),服務(wù)員在一旁等待(服務(wù)員線程睡眠)…
在此過(guò)程中,廚師和服務(wù)員兩個(gè)線程交替睡眠,廚師在做飯時(shí)服務(wù)員沒(méi)有端盤(pán)子(廚師線程運(yùn)行時(shí)服務(wù)員線程睡眠),服務(wù)員在端盤(pán)子時(shí)廚師沒(méi)有在做飯(服務(wù)員線程運(yùn)行時(shí)廚師線程睡眠),確保了數(shù)據(jù)的安全
根據(jù)廚師和服務(wù)員這個(gè)栗子,我們可以通過(guò)代碼來(lái)一步步實(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("小米粥","沒(méi)味道,不好吃");
}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é)果:
只截取了一部分,我們可以看到,“小米粥”并沒(méi)有每次都對(duì)應(yīng)“沒(méi)味道,不好吃”,“老北京雞肉卷”也沒(méi)有每次都對(duì)應(yīng)“甜辣味”,而是一種錯(cuò)亂的對(duì)應(yīng)關(guān)系
...
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:沒(méi)味道,不好吃
服務(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)度模式,沒(méi)有協(xié)調(diào),因此出現(xiàn)了該現(xiàn)象
以上運(yùn)行結(jié)果解釋如圖:

加入線程安全
針對(duì)上面的線程不安全問(wèn)題,對(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é)果:
由輸出可見(jiàn),又出現(xiàn)了新的問(wèn)題:
雖然加入了線程安全,set和get方法不再像前面一樣同時(shí)執(zhí)行并且菜名和味道一一對(duì)應(yīng),但是set和get方法并沒(méi)有交替執(zhí)行(通俗地講,不是廚師一做完服務(wù)員就端走),而是無(wú)序地執(zhí)行(廚師有可能做完之后繼續(xù)做,做好幾道,服務(wù)員端好幾次…無(wú)規(guī)律地做和端)
...
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
實(shí)現(xiàn)生產(chǎn)者與消費(fèi)者問(wèn)題
由上面可知,加入線程安全依舊無(wú)法實(shí)現(xiàn)該問(wèn)題。因此,要解決該問(wèn)題,回到前面的引入部分,嚴(yán)格按照生產(chǎn)者與消費(fèi)者問(wèn)題中所說(shuō)地去編寫(xiě)程序
生產(chǎn)者與消費(fèi)者問(wèn)題:
生產(chǎn)者和消費(fèi)者為兩個(gè)線程,兩個(gè)線程在運(yùn)行過(guò)程中交替睡眠,生產(chǎn)者在生產(chǎn)時(shí)消費(fèi)者沒(méi)有在消費(fèi),消費(fèi)者在消費(fèi)時(shí)生產(chǎn)者沒(méi)有在生產(chǎn),確保數(shù)據(jù)安全
↓
廚師在生產(chǎn)食品(廚師線程運(yùn)行)的過(guò)程中,服務(wù)員應(yīng)當(dāng)?shù)却ǚ?wù)員線程睡眠),等到食品生產(chǎn)完成(廚師線程結(jié)束)后將食品放入盤(pán)子中,服務(wù)員將盤(pán)子端出去(服務(wù)員線程運(yùn)行),此時(shí)沒(méi)有盤(pán)子可以放食品,因此廚師休息(廚師線程休眠),一段時(shí)間過(guò)后服務(wù)員將盤(pán)子拿回來(lái)(服務(wù)員線程結(jié)束),廚師開(kāi)始進(jìn)行生產(chǎn)食品(廚師線程運(yùn)行),服務(wù)員在一旁等待(服務(wù)員線程睡眠)…
↓
在此過(guò)程中,廚師和服務(wù)員兩個(gè)線程交替睡眠,廚師在做飯時(shí)服務(wù)員沒(méi)有端盤(pán)子(廚師線程運(yùn)行時(shí)服務(wù)員線程睡眠),服務(wù)員在端盤(pán)子時(shí)廚師沒(méi)有在做飯(服務(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為T(mén)rue(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置為T(mén)rue(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ù)員開(kāi)始端菜
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
作了以上調(diào)整之后的程序輸出:
我們可以看到,沒(méi)有出現(xiàn)數(shù)據(jù)錯(cuò)亂,并且菜的順序是交替依次進(jìn)行的
...
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務(wù)員端走的菜的名稱是:小米粥 味道:沒(méi)味道,不好吃
服務(wù)員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
...
這就是生產(chǎn)者與消費(fèi)者問(wèn)題的一個(gè)典型例子
總結(jié)
本篇文章就到這里了,希望能給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java爬蟲(chóng)Jsoup+httpclient獲取動(dòng)態(tài)生成的數(shù)據(jù)
這篇文章主要介紹了Java爬蟲(chóng)Jsoup+httpclient獲取動(dòng)態(tài)生成的數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2017-05-05
全面了解java byte數(shù)組與文件讀寫(xiě)
下面小編就為大家?guī)?lái)一篇全面了解java byte數(shù)組與文件讀寫(xiě)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
SpringBoot操作spark處理hdfs文件的操作方法
本文介紹了如何使用Spring Boot操作Spark處理HDFS文件,包括導(dǎo)入依賴、配置Spark信息、編寫(xiě)Controller和Service處理地鐵數(shù)據(jù)、運(yùn)行項(xiàng)目以及觀察Spark和HDFS的狀態(tài),感興趣的朋友跟隨小編一起看看吧2025-01-01
Java實(shí)現(xiàn)音頻轉(zhuǎn)文本的示例代碼(語(yǔ)音識(shí)別)
Java中實(shí)現(xiàn)音頻轉(zhuǎn)文本通常涉及使用專門(mén)的語(yǔ)音識(shí)別服務(wù),本文主要介紹了Java實(shí)現(xiàn)音頻轉(zhuǎn)文本的示例代碼(語(yǔ)音識(shí)別),具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05
最大子數(shù)組和Java實(shí)現(xiàn)代碼示例
這篇文章主要介紹了最大子數(shù)組和Java實(shí)現(xiàn)的相關(guān)資料,文中介紹了兩種方法來(lái)解決尋找具有最大和的連續(xù)子數(shù)組的問(wèn)題,第一種方法是動(dòng)態(tài)規(guī)劃,第二種方法是分治法,需要的朋友可以參考下2024-11-11
java數(shù)據(jù)結(jié)構(gòu)之棧的詳解
這篇文章主要為大家詳細(xì)介紹了Java數(shù)據(jù)結(jié)構(gòu)的棧的應(yīng)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能給你帶來(lái)幫助2021-08-08
SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證
雙因素身份驗(yàn)證2FA是一種安全系統(tǒng),要求用戶提供兩種不同的身份驗(yàn)證方式才能訪問(wèn)某個(gè)系統(tǒng)或服務(wù),國(guó)內(nèi)普遍做短信驗(yàn)證碼這種的用的比較少,不過(guò)在國(guó)外的網(wǎng)站中使用雙因素身份驗(yàn)證的還是很多的,這篇文章主要介紹了SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證,需要的朋友參考下2024-04-04
Spring解決循環(huán)依賴問(wèn)題的四種方法匯總
這篇文章主要介紹了Spring解決循環(huán)依賴問(wèn)題的四種方法匯總,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-07-07

