Java生產消費者模型——代碼解析

来源:https://www.cnblogs.com/bobozz/archive/2019/10/03/11620984.html
-Advertisement-
Play Games

我們將生產者、消費者、庫存、和調用線程的主函數分別寫進四個類中,通過搶奪非線程安全的數據集合來直觀的表達在進行生產消費者模型的過程中可能出現的問題與解決辦法。 我們假設有一個生產者,兩個消費者來共同搶奪庫存里的資源,而生產者和消費者都以線程來實現。 庫存對象只有是唯一的才會出現搶奪一個資源的可能,所 ...


我們將生產者、消費者、庫存、和調用線程的主函數分別寫進四個類中,通過搶奪非線程安全的數據集合來直觀的表達在進行生產消費者模型的過程中可能出現的問題與解決辦法。

我們假設有一個生產者,兩個消費者來共同搶奪庫存里的資源,而生產者和消費者都以線程來實現。

庫存對象只有是唯一的才會出現搶奪一個資源的可能,所以為了使庫存對象是唯一的,我們可以使用兩種方法實現,單例模式和通過生產者和消費者的構造函數參數來初始化。

本次舉例使用的是構造函數的方法,但代碼中也註釋出了單例模式的寫法與使用。

先創建一個簡單的生產消費者模型,查看它的運行結果。

  • 庫存類:

package producterac;

import java.util.ArrayList;

public class WareHouse {

    //存放非線程安全的數組的集合
    private ArrayList<String> list = new ArrayList<String>();
    
    
    /*
     * //創建單例模式使生產消費者操作的是同一庫存對象 
     * private WareHouse() {} 
     * //建立靜態對象以在初始化的時候建立僅一個庫存對象
     * private static WareHouse wh = new WareHouse();
     * 
     * //將方法設置為靜態是因為在無法new庫存對象的情況下, 
     * //我們可以通過將方法設定為靜態來直接通過類名調用靜態方法 
     * public static WareHouse getInstance() {
     *  return wh;
     * }
     */
    
    
    //寫生產者操作倉庫的方法
    public void add() {
        if(list.size() < 20) {
            list.add("一個數據");
        }else {
            //數據存夠之後直接返回,不運行存儲數據的操作
            return;
        }
    }
    
    //寫消費者操作倉庫的操作
    public void get() {
        //判斷集合中是否還有數據可以取出
        //如果不判斷會造成集合越界
        if(list.size() > 0) {
            list.remove(0);
        }else {
            return;
        }
    }
    
}
  • 生產者類:
package producterac;

public class Producter extends Thread{

    private String pName;
    
    //我們要使生產者和消費者操控同一個庫存對象
    //也可以使用單例模式來建立庫存對象
    private WareHouse wh;
    public Producter(String pName,WareHouse wh) {
        this.pName = pName;
        this.wh = wh;
    }
    
    
    //重寫run方法
    public void run() {
        while(true) {
            wh.add();
            System.out.println("生產者"+pName+"添加了一個貨物");
            try {
                //使線程等待一會兒
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
  • 消費者類:
package producterac;

public class Consumer extends Thread{

    private String cName;
    
    //獲取庫存對象
    /* private WareHouse wh = WareHouse.getInstance(); */
    
    //我們要使生產者和消費者操控同一個庫存對象
    //也可以使用單例模式來建立庫存對象
    private WareHouse wh;
    public Consumer(String cName,WareHouse wh) {
        this.cName = cName;
        this.wh = wh;
    }
    
    //重寫run方法
    public void run() {
        while(true) {
            wh.get();
            System.out.println("消費者"+cName+"拿走了一個貨物");
            try {
                //使線程等待一會兒
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
  • 主函數類:
package producterac;

public class Main {

    public static void main(String[] args) {
        
        WareHouse wh = new WareHouse();
        Producter p1 = new Producter("1", wh);
        Consumer c1 = new Consumer("1", wh);
        Consumer c2 = new Consumer("2", wh);
        
        p1.start();
        c1.start();
        c2.start();
    }
}

部分運行結果:

 

 

我們看到出現 java.lang.ArrayIndexOutOfBoundsException異常,說明消費者在拿走貨物的時候集合越界沒有拿到,所以出現了異常。

即使我們在庫存的get()方法中判斷了集合是否為空,但也還是出現了異常。原因是因為在兩個線程同時訪問一個對象的時候,有可能當線程1剛判斷完集合不為空進入了if迴圈但還沒有拿走貨物的情況下,線程2也進行了get()方法先線程1一步拿走了最後的一個貨物,然後當線程1想拿走貨物的時候集合里已經沒有了,這種情況下就會發生上述異常。

這就造成了線程搶奪資源時非安全的問題,那麼我們可以將庫存對象使用線程鎖synchronized鎖起來,這樣在一個消費者訪問庫存對象的時候其他消費者無法訪問庫存對象,從而解決集合越界問題,使線程安全。

  • 修改過的庫存類(加入了synchronized修飾符的add()和get()方法):
    //寫生產者操作倉庫的方法
    public synchronized void add() {
        if(list.size() < 20) {
            list.add("一個數據");
        }else {
            //數據存夠之後直接返回,不運行存儲數據的操作
            return;
        }
    }
    
    //寫消費者操作倉庫的操作
    public synchronized void get() {
        //判斷集合中是否還有數據可以取出
        //如果不判斷會造成集合越界
        if(list.size() > 0) {
            list.remove(0);
        }else {
            return;
        }
    }

 

使用synchronized修飾符修飾庫存方法之後就不會報錯了!

 

我們也可以將return替換為wait()方法讓線程等待,將編寫的生產消費者模型中的return修改為wait()。

  • 修改過的庫存類: 
    //寫生產者操作倉庫的方法
    public synchronized void add() {
        if(list.size() < 20) {
            list.add("一個數據");
        }else {
            try {
                //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    
    //寫消費者操作倉庫的操作
    public synchronized void get() {
        //判斷集合中是否還有數據可以取出
        //如果不判斷會造成集合越界
        if(list.size() > 0) {
            list.remove(0);
        }else {
            try {
                //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

運行結果:

 

 

 我們會發現到最後所有的線程都會處於wait等待狀態,運行到最後沒有線程在執行了。所以我們需要在其中一個線程等待的時候將其他線程繼續喚醒,保持系統的運行。

喚醒線程可以使用notify/notifyAll()方法。

  • 再次修改後的庫存類:
    //寫生產者操作倉庫的方法
    public synchronized void add() {
        if(list.size() < 20) {
            list.add("一個數據");
        }else {
            try {
                //因為我們無法知道哪個線程是消費者線程,所以我們要將線程全部喚醒
                this.notifyAll();
                //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    
    //寫消費者操作倉庫的操作
    public synchronized void get() {
        //判斷集合中是否還有數據可以取出
        //如果不判斷會造成集合越界
        if(list.size() > 0) {
            list.remove(0);
        }else {
            try {
                //因為我們無法知道哪個線程是生產者線程,所以我們要將線程全部喚醒
                this.notifyAll();
                //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

運行成功!說明我們這時候真正地實現了簡單的生產消費者模型。

 

附:如果將完成的生產消費者模型中add()和get()方法的synchronized修飾符去掉,會發生如下錯誤。

 

 

將synchronized修飾符去掉後,發生了java.lang.IllegalMonitorStateException異常,原因是當線程1進入else要執行wait()方法的那個時刻,線程2也進入了庫存對象中,致使當wait()方法真正執行的時候wait的是線程2而不是線程1,發生這種情況的時候就會發生上述異常。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、文本單行顯示溢出時出現省略號 二、文本多行顯示溢出時出現省略號(這種樣式只能在webkit和移動端使用,包括小程式,不能設置固定高度) 三、首行縮進兩字元 ...
  • 方法一、兩個div都設置 display: table-cell; 方法二、父級div設置 display: -webkit-box; ...
  • 熱愛旅行,一直想找到一個應用記錄我到過的新的“領土”。搜了下市面上已經存在的地圖應用,都不是我想要的。找不到趁手的工具,那就自己打造一把。如何打造一個自己的旅行足跡地圖? ...
  • 命令模式(Command): 將請求封裝成對象,以便使用不同的請求、日誌、隊列等來參數化其他對象。命令模式也支持撤銷操作。 命令模式的角色: 1)傳遞命令對象(Invoker):是請求的發送者,它通常擁有很多的命令對象,並通過訪問命令對象來執行相關請求,它不直接訪問接收者。 2)抽象命令介面(Com ...
  • 使用Python內置函數:bin()、oct()、int()、hex()可實現進位轉換。 先看Python官方文檔中對這幾個內置函數的描述: bin(x)Convert an integer number to a binary string. The result is a valid Pytho ...
  • "Flask的使用以及返回值(其中Response後續詳細單獨補充)" "Flask的路由解讀以及其配置" "Flask的請求擴展" "Flask中的cookie和session" "Flask中的request和response" "Flask中的渲染變數" "Flask中的CBV以及正則表達式" ...
  • 第一次寫博客,正好在回顧Java的時候用到了比較器,記錄一下使用的方法。 Java比較器多用於對象數組的排序,主要用到comparable和comparator介面 1、使用comparable介面 首先將需要實現排序對象的類實現comparable介面,實現後覆寫comparaTo(T other ...
  • 1、HashMap源碼解析(JDK8) 基礎原理: 對比上一篇《Java中的容器(集合)之ArrayList源碼解析》而言,本篇只解析HashMap常用的核心方法的源碼。 HashMap是一個以鍵值對存儲的容器。 hashMap底層實現為數組+鏈表+紅黑樹(鏈表超過8時轉為紅黑樹,JDK7為數組+鏈 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...