Java多線程之線程通信生產(chǎn)者消費(fèi)者模式及等待喚醒機(jī)制代碼詳解
前言
前面的例子都是多個(gè)線程在做相同的操作,比如4個(gè)線程都對(duì)共享數(shù)據(jù)做tickets–操作。大多情況下,程序中需要不同的線程做不同的事,比如一個(gè)線程對(duì)共享變量做tickets++操作,另一個(gè)線程對(duì)共享變量做tickets–操作,這就是大名鼎鼎的生產(chǎn)者和消費(fèi)者模式。
正文
一,生產(chǎn)者-消費(fèi)者模式也是多線程
生產(chǎn)者和消費(fèi)者模式也是多線程的范例。所以其編程需要遵循多線程的規(guī)矩。
首先,既然是多線程,就必然要使用同步。上回說(shuō)到,synchronized關(guān)鍵字在修飾函數(shù)的時(shí)候,使用的是“this”鎖,所以在同一個(gè)類中的函數(shù)被synchronized修飾后,使用的是同一把鎖。線程調(diào)用這些函數(shù)時(shí),不管調(diào)用的是tickets++操作函數(shù),還是tickets–函數(shù),都會(huì)先去判斷是否加鎖了,得到鎖之后再去進(jìn)行具體的操作。
我們先用代碼把程序中的資源,生產(chǎn)者,消費(fèi)者表示出來(lái)。
package com.jimmy.ThreadCommunication; class Resource{ // 資源類 private String productName; // 資源名稱 private int count = 1; // 資源編號(hào) public void produce(String name){ // 生產(chǎn)資源函數(shù) this.productName = name + count; count ++; // 資源編號(hào)遞增,用來(lái)模擬資源遞增 System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者.."+this.productName); } public void consume() { // 消費(fèi)資源函數(shù) System.out.println(Thread.currentThread().getName()+"...消費(fèi)者.."+this.productName); } } class Producer implements Runnable{ // 生產(chǎn)者類,用于開(kāi)啟生產(chǎn)者線程 private Resource res; //生產(chǎn)者初始化就要分配資源 public Producer(Resource res) { this.res = res; } @Override public void run() { for (int i = 0; i < 10; i++) { res.produce("bread"); // 循環(huán)生產(chǎn)10次 } } } class Comsumer implements Runnable{ // 消費(fèi)者類,用于開(kāi)啟消費(fèi)者線程 private Resource res; //同理,消費(fèi)者一初始化也要分配資源 public Comsumer(Resource res) { this.res = res; } @Override public void run() { for (int i = 0; i < 10; i++) { res.consume(); // 循環(huán)消費(fèi)10次 } } } public class ProducerAndConsumer1 { public static void main(String[] args) { Resource resource = new Resource(); // 實(shí)例化資源 Producer producer = new Producer(resource); // 實(shí)例化生產(chǎn)者和消費(fèi)者類,它們?nèi)〉猛粋€(gè)資源 Comsumer comsumer = new Comsumer(resource); Thread threadProducer = new Thread(producer); // 創(chuàng)建1個(gè)生產(chǎn)者線程 Thread threadComsumer = new Thread(comsumer); // 創(chuàng)建1個(gè)消費(fèi)者線程 threadProducer.start(); // 分別開(kāi)啟線程 threadComsumer.start(); } }
架子搭好了,就來(lái)運(yùn)行一下,當(dāng)然會(huì)出現(xiàn)錯(cuò)誤的結(jié)果,如下所示:
Thread-0...生產(chǎn)者..bread1 Thread-0...生產(chǎn)者..bread2 Thread-0...生產(chǎn)者..bread3 Thread-0...生產(chǎn)者..bread4 Thread-0...生產(chǎn)者..bread5 Thread-1...消費(fèi)者..bread1 Thread-1...消費(fèi)者..bread6 Thread-1...消費(fèi)者..bread6 Thread-1...消費(fèi)者..bread6 Thread-1...消費(fèi)者..bread6 Thread-1...消費(fèi)者..bread6 Thread-0...生產(chǎn)者..bread6 Thread-0...生產(chǎn)者..bread7 Thread-1...消費(fèi)者..bread6 Thread-1...消費(fèi)者..bread8 Thread-1...消費(fèi)者..bread8 Thread-1...消費(fèi)者..bread8 Thread-0...生產(chǎn)者..bread8 Thread-0...生產(chǎn)者..bread9 Thread-0...生產(chǎn)者..bread10
很明顯,出現(xiàn)了線程安全錯(cuò)誤。這時(shí),就需要“同步”來(lái)保證對(duì)共享變量的互斥訪問(wèn)。上面代碼中需要同步的就是Resource資源類中的produce和consume方法,分別使用synchronized來(lái)修飾,由于synchronized修飾方法時(shí)使用的是“this”鎖,所以同一個(gè)類中的所有被修飾的方法用的都是同一個(gè)鎖,那么線程一次只能訪問(wèn)其中一個(gè)方法。加鎖后的Resource類方法如下:
class Resource{ // 資源類 private String productName; // 資源名稱 private int count = 1; // 資源編號(hào) public synchronized void produce(String name){ // 生產(chǎn)資源函數(shù) this.productName = name + count; count ++; // 資源編號(hào)遞增,用來(lái)模擬資源遞增 System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者.."+this.productName); } public synchronized void consume() { // 消費(fèi)資源函數(shù) System.out.println(Thread.currentThread().getName()+"...消費(fèi)者.."+this.productName); } }
再來(lái)跑一次代碼,又出現(xiàn)問(wèn)題了:
Thread-0...生產(chǎn)者..bread1 Thread-0...生產(chǎn)者..bread2 Thread-0...生產(chǎn)者..bread3 Thread-0...生產(chǎn)者..bread4 Thread-0...生產(chǎn)者..bread5 Thread-0...生產(chǎn)者..bread6 Thread-0...生產(chǎn)者..bread7 Thread-0...生產(chǎn)者..bread8 Thread-0...生產(chǎn)者..bread9 Thread-0...生產(chǎn)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10 Thread-1...消費(fèi)者..bread10
雖然沒(méi)有了線程安全錯(cuò)誤,但是問(wèn)題來(lái)了,生產(chǎn)者不停的生產(chǎn),還沒(méi)等消費(fèi)者消費(fèi)呢,就將后面的資源覆蓋了前面的資源,導(dǎo)致消費(fèi)者消費(fèi)不到前面的資源,這樣很容易造成系統(tǒng)資源浪費(fèi)。理想中的結(jié)果應(yīng)該是,生產(chǎn)者生產(chǎn)一個(gè),消費(fèi)者消費(fèi)一個(gè),和諧運(yùn)行。對(duì)此,java為多線程引入了”等待-喚醒”機(jī)制。
二,等待喚醒機(jī)制
與線程做同樣的操作不同,不同線程之間的操作需要等待喚醒機(jī)制來(lái)保證線程間的執(zhí)行順序。生產(chǎn)者和消費(fèi)者模式中,生產(chǎn)者和消費(fèi)者是兩類不同的線程, 這兩類中又可以有很多線程來(lái)協(xié)同工作。通俗來(lái)說(shuō)就是,系統(tǒng)為資源設(shè)置一個(gè)標(biāo)志flag,該標(biāo)志用來(lái)標(biāo)明資源是否存在,所有的線程執(zhí)行操作前都要判斷資源是否存在。舉例來(lái)說(shuō),系統(tǒng)初始化后,資源是空的。接下來(lái)要執(zhí)行的可能是生產(chǎn)者線程,也可能是消費(fèi)者線程。如果是消費(fèi)者線程獲得執(zhí)行權(quán),先判斷資源,此時(shí)為空,就會(huì)進(jìn)入阻塞狀態(tài),交出執(zhí)行權(quán),并喚醒其他線程。如果是生產(chǎn)者線程獲得執(zhí)行權(quán),先判斷資源,此時(shí)為空,立馬進(jìn)行生產(chǎn),完了交出執(zhí)行權(quán)并喚醒其他線程。
注意,上面提到了兩點(diǎn),第一點(diǎn)是標(biāo)志位flag,也就是等待機(jī)制,生產(chǎn)者要判斷系統(tǒng)沒(méi)有資源才進(jìn)行生產(chǎn),不然要等待,消費(fèi)者要判斷系統(tǒng)有資源才進(jìn)行消費(fèi),不然也要等待。第二點(diǎn)是喚醒機(jī)制,不管是生產(chǎn)者還是消費(fèi)者,它們?cè)谏a(chǎn)完或者消費(fèi)完后,都要執(zhí)行一個(gè)喚醒操作。java提供的等待喚醒機(jī)制是由java.lang.Object類中的wait()和notify()函數(shù)組來(lái)實(shí)現(xiàn)的。其中notify()函數(shù)隨機(jī)喚醒一個(gè)被wait()的線程,而notifyAll()喚醒所有被wait()的線程。很遺憾,并沒(méi)有直接喚醒對(duì)方線程的函數(shù)。
notify()適用于單生產(chǎn)者和單消費(fèi)者模式,而notifyAll()適用于多生產(chǎn)者或多消費(fèi)者模式。
下面來(lái)看2個(gè)生產(chǎn)者和2個(gè)消費(fèi)者線程處理一個(gè)共享變量的代碼示例:
package com.jimmy.ThreadCommunication; class Resource2{ private String productName; private int count = 1; private boolean flag = false; // 資源類增加一個(gè)標(biāo)志位,默認(rèn)false,也就是沒(méi)有資源 public synchronized void produce(String name){ while (flag == true) { // 如果flag為true,也就是有資源了,生產(chǎn)者線程就去等待。 try { wait(); // wait函數(shù)拋出的異常只能被截獲 } catch (InterruptedException e) { e.printStackTrace(); } } this.productName = name + count; count ++; System.out.println(Thread.currentThread().getName()+"....生產(chǎn)者.."+this.productName); flag = true; // 生產(chǎn)完了就將flag修改為true notifyAll(); // 然后喚醒其他線程 } public synchronized void consume() { while (flag == false) { // 如果flag為false,也就是沒(méi)有資源,消費(fèi)者線程就去等待 try { // 判斷flag要用while,因?yàn)榫€程被喚醒后會(huì)再次判斷flag wait(); // 而如果是if來(lái)判斷,被喚醒后不會(huì)再判斷flag,那么多個(gè)生產(chǎn)者線程就可能死鎖 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"...消費(fèi)者.."+this.productName); flag = false; // 消費(fèi)完了就把標(biāo)志改為false notifyAll(); // 然后喚醒其他線程,因?yàn)橛卸鄠€(gè)生產(chǎn)者和消費(fèi)者線程,所以要用notifyAll, // 因?yàn)閚otify只喚醒一個(gè),喚醒到同類型的線程就不好了。 } } class Producer2 implements Runnable{ private Resource2 res; //生產(chǎn)者初始化就要分配資源 public Producer2(Resource2 res) { this.res = res; } @Override public void run() { for (int i = 0; i < 5; i++) { res.produce("bread"); } } } class Comsumer2 implements Runnable{ private Resource2 res; //同理,消費(fèi)者一初始化也要分配資源 public Comsumer2(Resource2 res) { this.res = res; } @Override public void run() { for (int i = 0; i < 10; i++) { res.consume(); } } } public class ProducerAndConsumer2 { public static void main(String[] args) { Resource2 resource = new Resource2(); // 實(shí)例化資源 Producer2 producer = new Producer2(resource); // 實(shí)例化生產(chǎn)者,并傳入資源對(duì)象 Comsumer2 comsumer = new Comsumer2(resource); // 實(shí)例化消費(fèi)者,并傳入相同的資源對(duì)象 Thread threadProducer1 = new Thread(producer); // 創(chuàng)建2個(gè)生產(chǎn)者線程 Thread threadProducer2 = new Thread(producer); Thread threadComsumer1 = new Thread(comsumer); // 創(chuàng)建2個(gè)消費(fèi)者線程 Thread threadComsumer2 = new Thread(comsumer); threadProducer1.start(); threadProducer2.start(); threadComsumer1.start(); threadComsumer2.start(); } }
上述代碼的輸出結(jié)果如下,是理想中的生產(chǎn)一個(gè),消費(fèi)一個(gè)依次進(jìn)行。
Thread-0....生產(chǎn)者..bread1 Thread-3...消費(fèi)者..bread1 Thread-1....生產(chǎn)者..bread2 Thread-2...消費(fèi)者..bread2 Thread-1....生產(chǎn)者..bread3 Thread-3...消費(fèi)者..bread3 Thread-0....生產(chǎn)者..bread4 Thread-3...消費(fèi)者..bread4 Thread-1....生產(chǎn)者..bread5 Thread-2...消費(fèi)者..bread5 Thread-1....生產(chǎn)者..bread6 Thread-3...消費(fèi)者..bread6 Thread-0....生產(chǎn)者..bread7 Thread-3...消費(fèi)者..bread7 Thread-1....生產(chǎn)者..bread8 Thread-2...消費(fèi)者..bread8 Thread-0....生產(chǎn)者..bread9 Thread-3...消費(fèi)者..bread9 Thread-0....生產(chǎn)者..bread10 Thread-2...消費(fèi)者..bread10
可以看出,線程0和1是生產(chǎn)者線程,他們每次只有一個(gè)進(jìn)行生產(chǎn)。線程2和3是消費(fèi)者線程,同樣的,每次只有一個(gè)進(jìn)行消費(fèi)。
注意,上述代碼中的問(wèn)題有2點(diǎn)需要注意,第一點(diǎn)是用if還是while來(lái)判斷flag,第二點(diǎn)是用notify還是notifyAll函數(shù)。統(tǒng)一來(lái)說(shuō),while判斷在線程喚醒后還會(huì)再次判斷,如果只有一個(gè)生產(chǎn)者和消費(fèi)者線程的話可以用if,如果有多個(gè)生產(chǎn)者或者消費(fèi)者,就必須用while判斷,不然會(huì)出現(xiàn)死鎖。所以,最終要用while和notifyAll()的組合。
總結(jié)
多線程編程往往是多個(gè)線程執(zhí)行不同的任務(wù),不同的任務(wù)不僅需要“同步”,還需要“等待喚醒機(jī)制”。兩者結(jié)合就可以實(shí)現(xiàn)多線程編程,其中的生產(chǎn)者消費(fèi)者模式就是經(jīng)典范例。
然而,使用synchronized修飾同步函數(shù)和使用Object類中的wait,notify方法實(shí)現(xiàn)等待喚醒是有弊端的。就是效率問(wèn)題,notifyAll方法喚醒所有被wait的線程,包括本類型的線程,如果本類型的線程被喚醒,還要再次判斷并進(jìn)入wait,這就產(chǎn)生了很大的效率問(wèn)題。理想狀態(tài)下,生產(chǎn)者線程要喚醒消費(fèi)者線程,而消費(fèi)者線程要喚醒生產(chǎn)者線程。為此,jdk1.5引入了java.util.concurrent.locks包,并提供了Lock和Condition接口及實(shí)現(xiàn)類。
以上就是本文關(guān)于Java多線程之線程通信生產(chǎn)者消費(fèi)者模式及等待喚醒機(jī)制代碼詳解的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:Java編程之多線程死鎖與線程間通信簡(jiǎn)單實(shí)現(xiàn)代碼、Java多線程編程小實(shí)例模擬停車場(chǎng)系統(tǒng)等,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
相關(guān)文章
超個(gè)性修改SpringBoot項(xiàng)目的啟動(dòng)banner的方法
這篇文章主要介紹了超個(gè)性修改SpringBoot項(xiàng)目的啟動(dòng)banner的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03spring項(xiàng)目如何配置多數(shù)據(jù)源(已上生產(chǎn),親測(cè)有效)
這篇文章主要介紹了spring項(xiàng)目如何配置多數(shù)據(jù)源(已上生產(chǎn),親測(cè)有效),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12使用spring-cache一行代碼解決緩存擊穿問(wèn)題
本文主要介紹了使用spring-cache一行代碼解決緩存擊穿問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04SpringBoot 配合 SpringSecurity 實(shí)現(xiàn)自動(dòng)登錄功能的代碼
這篇文章主要介紹了SpringBoot 配合 SpringSecurity 實(shí)現(xiàn)自動(dòng)登錄功能的代碼,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Spring Boot學(xué)習(xí)入門之統(tǒng)一異常處理詳解
我們?cè)谧鯳eb應(yīng)用的時(shí)候,請(qǐng)求處理過(guò)程中發(fā)生錯(cuò)誤是非常常見(jiàn)的情況。下面這篇文章主要給大家介紹了關(guān)于Spring Boot學(xué)習(xí)入門之統(tǒng)一異常處理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-09-09Java中如何使用?byte?數(shù)組作為?Map?的?key
本文將討論在使用HashMap時(shí),當(dāng)byte數(shù)組作為key時(shí)所遇到的問(wèn)題及其解決方案,介紹使用String和List這兩種數(shù)據(jù)結(jié)構(gòu)作為臨時(shí)解決方案的方法,感興趣的朋友跟隨小編一起看看吧2023-06-06