「JAVA」通過實現(xiàn)生產(chǎn)者闸溃、消費者案例再次實踐Java 多線程

線程通信,在多線程系統(tǒng)中拱撵,不同的線程執(zhí)行不同的任務辉川;如果這些任務之間存在聯(lián)系,那么執(zhí)行這些任務的線程之間就必須能夠通信拴测,共同協(xié)調完成系統(tǒng)任務乓旗。

線程通信

生產(chǎn)者、消費者案例

案例分析

在案例中明集索,蔬菜基地作為生產(chǎn)者屿愚,負責生產(chǎn)蔬菜汇跨,并向超市輸送生產(chǎn)的蔬菜;消費者通過向超市購買獲得蔬菜妆距;超市怎作為生產(chǎn)者和消費者之間的共享資源穷遂,都會和超市有聯(lián)系;蔬菜基地娱据、共享資源蚪黑、消費者之間的交互流程如下:

生產(chǎn)者、消費者案例

在這個案例中吸耿,為什么不設計成生產(chǎn)者直接與給消費者交互祠锣?讓兩者直接交換數(shù)據(jù)不是更好嗎,選擇先先把數(shù)據(jù)存儲到共享資源中咽安,然后消費者再從共享資源中取出數(shù)據(jù)使用伴网,中間多了一個環(huán)節(jié)不是更麻煩了?

其實不是的妆棒,設計成這樣是有原因的澡腾,因為這樣設計很好的體現(xiàn)了面向對象的低耦合的設計理念;通過這樣實現(xiàn)的程序能更加符合人的操作理念糕珊,更加貼合現(xiàn)實環(huán)境动分;同時,也能很好的避免因生產(chǎn)者與消費者直接交互而導致的操作不安全的問題红选。

我們來對高耦合和低耦合做一個對比就會很直觀了:

  • 高(緊)耦合:生產(chǎn)者與消費者直接交互澜公,生產(chǎn)者(蔬菜基地)把蔬菜直接給到給消費者,雙方之間的依賴程度很高喇肋;此時坟乾,生產(chǎn)者中就必須持有消費者對象的引用,同樣的道理蝶防,消費者也必須要持有生產(chǎn)者對象的引用甚侣;這樣,消費者和生產(chǎn)者才能夠直接交互间学。

  • 低(松)耦合:引入一個中間對象(共享資源)來殷费,將生產(chǎn)者、消費者中需要對外輸出或者從外數(shù)據(jù)的操作封裝到中間對象中低葫,這樣详羡,消費者和生產(chǎn)者將會持有這個中間對象的引用,屏蔽了生產(chǎn)者和消費者直接的數(shù)據(jù)交互.氮采,大大見減小生產(chǎn)者和消費者之間的依賴程度殷绍。

關于高耦合和低耦合的區(qū)別,電腦中主機中的集成顯卡和獨立顯卡也是一個非常好的例子鹊漠。

  • 集成顯卡普遍都集成于CPU中主到,所以如果集成顯卡出現(xiàn)了問題需要更換,那么會連著CPU一塊更換躯概,其維護成本與CPU其實是一樣的登钥;

  • 獨立顯卡需要插在主板的顯卡接口上才能與計算機通信,其相對于整個計算機系統(tǒng)來說娶靡,是獨立的存在牧牢,即便出現(xiàn)問題需要更換,也只更換顯卡即可姿锭。

案例的代碼實現(xiàn)

接下來我們使用多線程技術實現(xiàn)該案例塔鳍,案例代碼如下:

蔬菜基地對象,VegetableBase.java

// VegetableBase.java

// 蔬菜基地
public class VegetableBase implements Runnable {

    // 超市實例
    private Supermarket supermarket = null;

    public VegetableBase(Supermarket supermarket) {
        this.supermarket = supermarket;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                supermarket.push("黃瓜", 1300);
                System.out.println("push : 黃瓜 " + 1300);
            } else {
                supermarket.push("青菜", 1400);
                System.out.println("push : 青菜 " + 1400);
            }
        }
    }
}

消費者對象呻此,Consumer.java

// Consumer.java

// 消費者
public class Consumer implements Runnable {

    // 超市實例
    private Supermarket supermarket = null;

    public Consumer(Supermarket supermarket) {
        this.supermarket = supermarket;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            supermarket.popup();
        }
    }
}

超市對象轮纫,Supermarket.java

// Supermarket.java

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數(shù)量
    private Integer num;

    // 蔬菜基地想超市輸送蔬菜
    public void push(String name, Integer num) {
        this.name = name;
        this.num = num;
    }

    // 用戶從超市中購買蔬菜
    public void popup() {
        // 為了讓效果更明顯,在這里模擬網(wǎng)絡延遲
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        System.out.println("蔬菜:" + this.name + ", " + this.num + "顆焚鲜。");
    }

}

運行案例掌唾,App.java

// 案例應用入口
public class App {

    public static void main(String[] args) {
        // 創(chuàng)建超市實例
        Supermarket supermarket = new Supermarket();
        // 蔬菜基地線程啟動, 開始往超市輸送蔬菜
        new Thread(new VegetableBase(supermarket)).start();
        new Thread(new VegetableBase(supermarket)).start();
        // 消費者線程啟動,消費者開始購買蔬菜
        new Thread(new Consumer(supermarket)).start();
        new Thread(new Consumer(supermarket)).start();
    }

}

發(fā)現(xiàn)了問題

運行該案例忿磅,打印出運行結果糯彬,外表一片祥和,可還是被敏銳的發(fā)現(xiàn)了問題葱她,問題如下所示:

案例運行中發(fā)現(xiàn)的問題

在一片看似祥和的打印結果中撩扒,出現(xiàn)了一個很不祥和的特例,生產(chǎn)基地在輸送蔬菜時吨些,黃瓜的數(shù)量一直都是1300顆搓谆,青菜的數(shù)量一直是1400顆,但是在消費者消費時卻出現(xiàn)了蔬菜名稱是黃瓜的锤灿,但數(shù)量卻是青菜的數(shù)量的情況挽拔。

之所以出現(xiàn)這樣的問題,是因為在本案例共享的資源中但校,多個線程共同競爭資源時沒有使用同步操作螃诅,而是異步操作,今兒導致了資源分配紊亂的情況状囱;需要注意的是术裸,并不是因為我們在案例中使用Thread.sleep();模擬網(wǎng)絡延遲才導致問題出現(xiàn),而是本來就存在問題亭枷,使用Thread.sleep();只是讓問題更加明顯袭艺。

案例問題的解決

在本案例中需要解決的問題有兩個,分別如下:

  1. 問題一:蔬菜名稱和數(shù)量不匹配的問題叨粘。
  2. 問題二:需要保證超市無貨時生產(chǎn)猾编,超市有貨時才消費瘤睹。

針對問題一解決方案:保證蔬菜基地在輸送蔬菜的過程保持同步,中間不能被其他線程(特別是消費者線程)干擾答倡,打亂輸送操作轰传;直至當前線程完成輸送后,其他線程才能進入操作瘪撇,同樣的获茬,當有線程進入操作后,其他線程只能在操作外等待倔既。

所以恕曲,技術方案可以使用同步代碼塊/同步方法/Lock機制來保持操作的同步性。

針對問題二的解決方案:給超市一個有無貨的狀態(tài)標志渤涌,

  • 超市無貨時佩谣,蔬菜基地輸送蔬菜補貨,此時生產(chǎn)基地線程可操作歼捏;

  • 超市有貨時稿存,消費者線程可操作;就是:保證生產(chǎn)基地 ——> 共享資源 ——> 消費者這個整個流程的完整運行瞳秽。

技術方案:使用線程中的等待和喚醒機制瓣履。

同步操作,分為同步代碼塊同步方法兩種练俐。詳情可查看我的另外一篇關于多線程的文章:「JAVA」Java 線程不安全分析袖迎,同步鎖和Lock機制,哪個解決方案更好

  1. 在同步代碼塊中的同步鎖必須選擇多個線程共同的資源對象腺晾,當前生產(chǎn)者線程在生產(chǎn)數(shù)據(jù)的時候(先擁有同步鎖)燕锥,其他線程就在鎖池中等待獲取鎖;當生產(chǎn)者線程執(zhí)行完同步代碼塊的時候悯蝉,就會釋放同步鎖归形,其他線程開始搶鎖的使用權,搶到后就會擁有該同步鎖鼻由,執(zhí)行完成后釋放暇榴,其他線程再開始搶鎖的使用權,依次往復執(zhí)行蕉世。
  2. 多個線程只有使用同一個對象(就好比案例中的共享資源對象)的時候蔼紧,多線程之間才有互斥效果,我們把這個用來做互斥的對象稱之為同步監(jiān)聽對象狠轻,又稱同步監(jiān)聽器奸例、互斥鎖、同步鎖向楼,同步鎖是一個抽象概念查吊,可以理解為在對象上標記了一把鎖谐区。
  3. 同步鎖對象可以選擇任意類型的對象即可,只需要保證多個線程使用的是相同鎖對象即可菩貌。在任何時候卢佣,最多只能運行一個線程擁有同步鎖重荠。因為只有同步監(jiān)聽鎖對象才能調用waitnotify方法箭阶,waitnotify方法存在于Object類中。

線程通信之 wait和notify方法

java.lang.Object中提供了用于操作線程通信的方法戈鲁,詳情如下:

  • wait()執(zhí)行該方法的線程對象會釋放同步鎖仇参,然后JVM把該線程存放到等待池中,等待著其他線程來喚醒該線程婆殿;
  • notify()執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的的任意一個線程诈乒,把線程轉到同步鎖池中等待;
  • notifyAll()執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的所有的線程婆芦,把這些線程轉到同步鎖池中等待怕磨;

注意:上述方法只能被同步監(jiān)聽鎖對象來調用,否則發(fā)生 IllegalMonitorStateException消约。

wait和notify方法應用實例

假設A線程B線程共同操作一個X對象(同步鎖)肠鲫,A、B線程可以通過X對象waitnotify方法來進行通信或粮,流程如下:

  1. A線程執(zhí)行X對象的同步方法時导饲,A線程持有X對象的鎖,B線程沒有執(zhí)行機會氯材,此時的B線程會在X對象的鎖池中等待渣锦;
  2. A線程在同步方法中執(zhí)行X.wait()方法時,A線程會釋放X對象的同步鎖氢哮,然后進入X對象的等待池中袋毙;
  3. 接著,在X對象的鎖池中等待鎖的B線程獲取X對象的鎖冗尤,執(zhí)行X的另一個同步方法听盖;
  4. B線程在同步方法中執(zhí)行X.notify()方法時,JVM會把A線程X對象的等待池中轉到X對象的同步鎖池中生闲,等待獲取鎖的使用權媳溺;
  5. B線程執(zhí)行完同步方法后,會釋放擁有的鎖碍讯,然后A線程獲得鎖悬蔽,繼續(xù)執(zhí)行同步方法;

基于上述機制捉兴,我們就可以使用同步操作 + wait和notify方法來解決案例中的問題了蝎困,重新來實現(xiàn)共享資源——超市對象:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數(shù)量
    private Integer num;
    // 超市是否為空
    private Boolean isEmpty = true;

    // 蔬菜基地向超市輸送蔬菜
    public synchronized void push(String name, Integer num) {
        try {
            while (!isEmpty) {   // 超市有貨時录语,不再輸送蔬菜,而是要等待消費者獲取
                   this.wait();  
             }
                this.name = name;
                this.num = num;
            isEmpty = false;
            this.notify();              // 喚醒另一個線程
        } catch(Exception e) {
            
        }
        
    }

    // 用戶從超市中購買蔬菜
    public synchronized void popup() {
        
        try {
            while (isEmpty) { // 超市無貨時禾乘,不再提供消費澎埠,而是要等待蔬菜基地輸送
                   this.wait();
            }
            // 為了讓效果更明顯,在這里模擬網(wǎng)絡延遲
            Thread.sleep(1000);
            System.out.println("蔬菜:" + this.name + ", " + this.num + "顆始藕。");
            isEmpty = true;
            this.notify();  // 喚醒另一線程
        } catch (Exception e) {

        }   
    }
}

線程通信之 使用Lock和Condition接口

由于waitnotify方法蒲稳,只能被同步監(jiān)聽鎖對象來調用,否則發(fā)生
IllegalMonitorStateException伍派。從Java 5開始江耀,提供了Lock機制,同時還有處理Lock機制的通信控制的Condition接口诉植。Lock機制沒有同步鎖的概念祥国,也就沒有自動獲取鎖和自動釋放鎖的這樣的操作了。

因為沒有同步鎖晾腔,所以Lock機制中的線程通信就不能調用waitnotify方法了舌稀;同樣的,Java 5 中也提供了解決方案灼擂,因此從Java 5開始壁查,可以:

  1. 使用Lock機制取代synchronized 代碼塊synchronized 方法
  2. 使用Condition接口對象的await缤至、signal潮罪、signalAll方法取代Object類中的wait、notify领斥、notifyAll方法嫉到;

Lock和Condition接口的性能也比同步操作要高很多,所以這種方式也是我們推薦使用的方式月洛。

我們可以使用Lock機制和Condition接口 方法來解決案例中的問題何恶,重新來實現(xiàn)的共享資源——超市對象,代碼如下:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數(shù)量
    private Integer num;
    // 超市是否為空
    private Boolean isEmpty = true;
        // lock
        private final Lock lock = new ReentrantLock();
        // Condition
        private Condition condition = lock.newCondition();
        

    // 蔬菜基地向超市輸送蔬菜
    public synchronized void push(String name, Integer num) {
        lock.lock(); // 獲取鎖
        try {
            while (!isEmpty) {   // 超市有貨時嚼黔,不再輸送蔬菜细层,而是要等待消費者獲取
                   condition.await();  
             }
                this.name = name;
                this.num = num;
            isEmpty = false;
            condition.signalAll();              
        } catch(Exception e) {
            
        } finally {
                lock.unlock();  // 釋放鎖
        }
        
    }

    // 用戶從超市中購買蔬菜
    public synchronized void popup() {
        lock.lock();
        try {
            while (isEmpty) { // 超市無貨時,不再提供消費唬涧,而是要等待蔬菜基地輸送
                   condition.await();
            }
            // 為了讓效果更明顯疫赎,在這里模擬網(wǎng)絡延遲
            Thread.sleep(1000);
            System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
            isEmpty = true;
            condition.signalAll();  
        } catch (Exception e) {
                
        }   finally {
                lock.unlock();
        }
    }
}

完結碎节,老夫雖不正經(jīng)捧搞,但老夫一身的才華!關注我,獲取更多編程科技知識胎撇。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末介粘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子晚树,更是在濱河造成了極大的恐慌姻采,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爵憎,死亡現(xiàn)場離奇詭異慨亲,居然都是意外死亡,警方通過查閱死者的電腦和手機纲堵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門巡雨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人席函,你說我怎么就攤上這事「越В” “怎么了茂附?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長督弓。 經(jīng)常有香客問我营曼,道長,這世上最難降的妖魔是什么愚隧? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任蒂阱,我火速辦了婚禮,結果婚禮上狂塘,老公的妹妹穿的比我還像新娘录煤。我一直安慰自己,他們只是感情好荞胡,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布妈踊。 她就那樣靜靜地躺著,像睡著了一般泪漂。 火紅的嫁衣襯著肌膚如雪廊营。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天萝勤,我揣著相機與錄音露筒,去河邊找鬼。 笑死敌卓,一個胖子當著我的面吹牛慎式,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瞬捕,長吁一口氣:“原來是場噩夢啊……” “哼鞍历!你這毒婦竟也來了?” 一聲冷哼從身側響起肪虎,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤劣砍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扇救,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刑枝,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年迅腔,在試婚紗的時候發(fā)現(xiàn)自己被綠了装畅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡沧烈,死狀恐怖掠兄,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情锌雀,我是刑警寧澤蚂夕,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站腋逆,受9級特大地震影響婿牍,放射性物質發(fā)生泄漏。R本人自食惡果不足惜惩歉,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一等脂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撑蚌,春花似錦上遥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至第煮,卻和暖如春解幼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背包警。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工撵摆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人害晦。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓特铝,卻偏偏與公主長得像暑中,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鲫剿,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355