溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java中怎么實(shí)現(xiàn)多線程通信

發(fā)布時(shí)間:2021-07-29 16:55:01 來源:億速云 閱讀:167 作者:Leah 欄目:開發(fā)技術(shù)

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é)果解釋如圖:

Java中怎么實(shí)現(xiàn)多線程通信

加入線程安全

針對(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)生產(chǎn)者與消費(fèi)者問題

由上面可知,加入線程安全依舊無法實(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 中的方法:

變量和類型方法描述
voidnotify()喚醒當(dāng)前this下的單個(gè)線程
voidnotifyAll()喚醒當(dāng)前this下的所有線程
voidwait()當(dāng)前線程休眠
voidwait(long timeoutMillis)當(dāng)前線程休眠一段時(shí)間
voidwait(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è)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(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)容。

AI