Java多線程通信問題深入瞭解

概述

多線程通信問題,也就是生產者與消費者問題

生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全

以下為百度百科對於該問題的解釋:

生產者與消費者問題:
生產者消費者問題(Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述瞭兩個共享固定大小緩沖區的線程——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩沖區中,然後重復此過程。與此同時,消費者也在緩沖區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩沖區滿時加入數據,消費者也不會在緩沖區中空時消耗數據。

解決辦法:
要解決該問題,就必須讓生產者在緩沖區滿時休眠(要麼幹脆就放棄數據),等到下次消費者消耗緩沖區中的數據的時候,生產者才能被喚醒,開始往緩沖區添加數據。同樣,也可以讓消費者在緩沖區空時進入休眠,等到生產者往緩沖區添加數據之後,再喚醒消費者。通常采用進程間通信的方法解決該問題,常用的方法有信號燈法等。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。

引入

該過程可以類比為一個栗子:

廚師為生產者,服務員為消費者,假設隻有一個盤子盛放食品。

廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)後將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過後服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…

在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保瞭數據的安全

根據廚師和服務員這個栗子,我們可以通過代碼來一步步實現

  • 定義廚師線程
 /**
     * 廚師,是一個線程
     */
    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;
            //加瞭這段之後,有可能這個地方的時間片更有可能被搶走,從而執行不瞭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關鍵字,實現線程安全,即:當一個線程正在執行時,另外的線程不會執行,在後面排隊等待當前的程序執行完後再執行

代碼如下所示,分別給兩個方法添加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方法不再像前面一樣同時執行並且菜名和味道一一對應,但是set和get方法並沒有交替執行(通俗地講,不是廚師一做完服務員就端走),而是無序地執行(廚師有可能做完之後繼續做,做好幾道,服務員端好幾次…無規律地做和端)


服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味

實現生產者與消費者問題

由上面可知,加入線程安全依舊無法實現該問題。因此,要解決該問題,回到前面的引入部分,嚴格按照生產者與消費者問題中所說地去編寫程序

生產者與消費者問題:
生產者和消費者為兩個線程,兩個線程在運行過程中交替睡眠,生產者在生產時消費者沒有在消費,消費者在消費時生產者沒有在生產,確保數據安全

廚師在生產食品(廚師線程運行)的過程中,服務員應當等待(服務員線程睡眠),等到食品生產完成(廚師線程結束)後將食品放入盤子中,服務員將盤子端出去(服務員線程運行),此時沒有盤子可以放食品,因此廚師休息(廚師線程休眠),一段時間過後服務員將盤子拿回來(服務員線程結束),廚師開始進行生產食品(廚師線程運行),服務員在一旁等待(服務員線程睡眠)…

在此過程中,廚師和服務員兩個線程交替睡眠,廚師在做飯時服務員沒有端盤子(廚師線程運行時服務員線程睡眠),服務員在端盤子時廚師沒有在做飯(服務員線程運行時廚師線程睡眠),確保數據的安全

需要用到的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表示服務員端菜,廚師休眠),這樣廚師在生產完之後不會繼續生產,避免瞭廚師兩次生產、服務員端走一份的情況

然後喚醒在當前this下休眠的所有進程,而廚師線程進行休眠

		public synchronized void setNameAneTaste(String name,String taste){
            if(flag){//當標記為true時,表示廚師可以生產,該方法才執行
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;//生產完之後,標記置為false,這樣廚師在生產完之後不會繼續生產,避免瞭廚師兩次生產、服務員端走一份的情況
                this.notifyAll();//喚醒在當前this下休眠的所有進程
                try {
                    this.wait();//此時廚師線程進行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }//end set
  • 對get方法進行修改

當且僅當flag為False(False表示服務員端菜,廚師休眠)時,才能進行端菜操作

端菜結束時,將flag置為True(True表示廚師生產,服務員休眠),這樣服務員在端完菜之後不會繼續端菜,避免瞭服務員兩次端菜、廚師生產一份的情況

然後喚醒在當前this下休眠的所有進程,而服務員線程進行休眠

        public synchronized void get(){
            if(!flag){//廚師休眠的時候,服務員開始端菜
                System.out.println("服務員端走的菜的名稱是:" + this.name + " 味道:" + this.taste);
                flag = true;//端完之後,標記置為true,這樣服務員在端完菜之後不會繼續端菜,避免瞭服務員兩次端菜、廚師隻生產一份的情況
                this.notifyAll();//喚醒在當前this下休眠的所有進程
                try {
                    this.wait();//此時服務員線程進行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }// end if
        }//end get

作瞭以上調整之後的程序輸出:

我們可以看到,沒有出現數據錯亂,並且菜的順序是交替依次進行的


服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味
服務員端走的菜的名稱是:小米粥 味道:沒味道,不好吃
服務員端走的菜的名稱是:老北京雞肉卷 味道:甜辣味

這就是生產者與消費者問題的一個典型例子

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: