面試官:小伙子钻趋,你給我簡單說一下生產(chǎn)者與消費者模型吧

前言

生產(chǎn)者-消費者模式是一個經(jīng)典的多線程設(shè)計模式。
在生產(chǎn)者-消費者模式中剂习,通常有兩類線程蛮位,即若干個生產(chǎn)者和消費者線程。

  • 生產(chǎn)者線程負(fù)責(zé)提交用戶請求
  • 消費者線程負(fù)責(zé)處理生產(chǎn)者提交的任務(wù)鳞绕。
  • 內(nèi)存緩沖區(qū) 緩存生產(chǎn)者提交的任務(wù)或數(shù)據(jù)失仁,供消費者使用。

開發(fā)需要解決的問題:

  1. 生產(chǎn)者線程與消費者線程對內(nèi)存緩沖區(qū)的操作的線程安全問題们何。
  2. 虛假喚醒萄焦。

測試:

/**
 * 生產(chǎn)者與消費者案例。
 * @author 
 */
public class TestProductorAndConsumer {

    public static void main(String[] args) {
        //創(chuàng)建職員
        Clerk clerk = new Clerk();

        //創(chuàng)建生產(chǎn)者與消費者線程
        Productor productor = new Productor(clerk);
        Consumer consumer = new Consumer(clerk);

        new Thread(productor, "生產(chǎn)者1").start();
        new Thread(consumer, "消費者1").start();
        new Thread(productor, "生產(chǎn)者2").start();
        new Thread(consumer, "消費者2").start();
    }
}

// Clerk職員
class Clerk {
    private int product = 0;    //產(chǎn)品數(shù)量
    private int capacity = 4;   // 容量
    // 進貨

    public synchronized void get() {

        if (product >= capacity) {
            System.out.println("產(chǎn)品已滿冤竹!");

            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
            this.notifyAll();
        }
    }

    // 賣貨
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("缺貨拂封!");

            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
            this.notifyAll();
        }
    }
}

// 生產(chǎn)者
class Productor implements Runnable {
    private Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {

        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.get();
        }
    }
}

// 消費者
class Consumer implements Runnable {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}

這就是一個簡單的生產(chǎn)者與消費者模型。
我們在Clerk類中給get()方法和sale()方法加了synchronized修飾符鹦蠕,來保證線程同步冒签。

但是運行后發(fā)現(xiàn)程序并沒有運行結(jié)束,分析發(fā)現(xiàn)钟病,我們的生產(chǎn)者線程最后沒有被喚醒萧恕,導(dǎo)致程序沒有結(jié)束刚梭。

對程序做一下修改:

/**
 * 對Clerk類的get()與sale()方法做一點修改
 */
public synchronized void get() {
        if (product >= capacity) {
            System.out.println("產(chǎn)品已滿!");

            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();

    }

    // 賣貨
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("缺貨票唆!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }

把notifyAll() 方法提到了else外面朴读,保證每個線程結(jié)束都能調(diào)用notifyAll()方法,運行一下惰说,發(fā)現(xiàn)程序確實能結(jié)束磨德,但是程序 product 變成了負(fù)數(shù)。這是由于調(diào)用notifyAll()喚醒了消費者模型吆视,執(zhí)行–product導(dǎo)致典挑。

我們來看一下wait()這個方法:


這就是我們要解決的虛假喚醒問題!@舶伞您觉!。
文檔提醒我們使用循環(huán)授滓。再對程序做一點修改

    // 進貨
    public synchronized void get() {

        // 使用while防止虛假喚醒
        while(product >= capacity) {
            System.out.println("產(chǎn)品已滿!");

            try {
                // 在一個參數(shù)版本中般堆,中斷和虛假的喚醒是可能的在孝,這個方法應(yīng)該總是在循環(huán)中使用:
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 除非product<capacity,否則不執(zhí)行++product操作
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();

    }

    // 賣貨
    public synchronized void sale() {
        while (product <= 0) {
            System.out.println("缺貨!");

            try {
                // 在一個參數(shù)版本中淮摔,中斷和虛假的喚醒是可能的私沮,這個方法應(yīng)該總是在循環(huán)中使用:
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 除非product>capacity,否則不執(zhí)行--product操作
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }

將 if 改為 while 循環(huán)后,
生產(chǎn)者線程被喚醒后進行判斷:如果product >= capacity和橙,則繼續(xù)調(diào)用wait()等待仔燕,直到再次被喚醒。如果product < capacity, 則執(zhí)行++product魔招。
同理消費者線程被喚醒后也會進行判斷晰搀,不滿足條件會繼續(xù)等待,直到再次被喚醒办斑。滿足條件后處理任務(wù)外恕。

至此,我們的生產(chǎn)者-消費者模型就圓滿完成了乡翅。

我們再對程序做一點修改鳞疲,不使用synchronized來修飾方法,而是采用可重入鎖ReentrantLock來手動加鎖與釋放鎖峦朗。此時我們也就不能再使用wait()和notifyAll()方法了,因為這兩個方法synchronized關(guān)鍵字合作使用建丧。
此處我們需要使用Condition條件。

直接看代碼:

// 進貨
    public void get() {
        lock.lock();
        try {
            // 使用while防止虛假喚醒
            while(product >= capacity) {
                System.out.println("產(chǎn)品已滿波势!");
                try {
                    // 在一個參數(shù)版本中翎朱,中斷和虛假的喚醒是可能的橄维,這個方法應(yīng)該總是在循環(huán)中使用:
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 除非product<capacity,否則不執(zhí)行++product操作
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }

    // 賣貨
    public void sale() {
        lock.lock();
        try {
            while (product <= 0) {
                System.out.println("缺貨!");

                try {
                    // 在一個參數(shù)版本中拴曲,中斷和虛假的喚醒是可能的争舞,這個方法應(yīng)該總是在循環(huán)中使用:
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 除非product>capacity,否則不執(zhí)行--product操作
            System.out.println(Thread.currentThread().getName() + " : " + --product);

            condition.signalAll();
        }finally {
            lock.unlock();
        }

    }

我們定義了一個可重入鎖 ReentrantLock lock, 通過lock.lock()來加鎖,通過lock.unlock()來釋放鎖澈灼,讓加鎖范圍更加靈活竞川。

這里提一下Condition接口提供的方法:

  • await():方法會使當(dāng)前線程等待,同時釋放當(dāng)前鎖叁熔。當(dāng)其他線程使用signal()或signalAll()方法時委乌,線程會重新獲得鎖并繼續(xù)執(zhí)行。當(dāng)線程被中斷時荣回,也能跳出等待遭贸。
  • await(long time,TimeUnit unit):方法會使線程等待,直到其他方法調(diào)用aignal()或者signalAll() 或者被中斷心软,或者等待超過設(shè)置的時間
  • awaitUninterruptibly():方法與await()基本相同析桥,但它并不會在等待的時候響應(yīng)中斷蔫慧。
  • signal():喚醒一個等待的線程。如果有線程正在等待此條件竭沫,則選擇一個線程進行喚醒爬泥。然后唱矛,該線程必須在從await返回之前重新獲取鎖误证。
  • signalAll():喚醒所有等待的線程会放。如果有線程在這種情況下等待,那么它們將被喚醒胖秒。每個線程必須重新獲取鎖缎患,然后才能從await返回慕的。

最后

感謝你看到這里阎肝,文章有什么不足還請指正,覺得文章對你有幫助的話記得給我點個贊肮街,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊风题,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嫉父,一起剝皮案震驚了整個濱河市沛硅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绕辖,老刑警劉巖摇肌,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異仪际,居然都是意外死亡围小,警方通過查閱死者的電腦和手機昵骤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肯适,“玉大人变秦,你說我怎么就攤上這事】蛱颍” “怎么了蹦玫?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刘绣。 經(jīng)常有香客問我樱溉,道長,這世上最難降的妖魔是什么纬凤? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任饺窿,我火速辦了婚禮,結(jié)果婚禮上移斩,老公的妹妹穿的比我還像新娘肚医。我一直安慰自己,他們只是感情好向瓷,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布肠套。 她就那樣靜靜地躺著,像睡著了一般猖任。 火紅的嫁衣襯著肌膚如雪你稚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天朱躺,我揣著相機與錄音刁赖,去河邊找鬼。 笑死长搀,一個胖子當(dāng)著我的面吹牛宇弛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播源请,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼枪芒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谁尸?” 一聲冷哼從身側(cè)響起舅踪,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎良蛮,沒想到半個月后抽碌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡决瞳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年货徙,在試婚紗的時候發(fā)現(xiàn)自己被綠了泽裳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡破婆,死狀恐怖涮总,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祷舀,我是刑警寧澤瀑梗,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站裳扯,受9級特大地震影響抛丽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饰豺,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一亿鲜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冤吨,春花似錦蒿柳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至怠李,卻和暖如春圾叼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捺癞。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工夷蚊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人髓介。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓惕鼓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親版保。 傳聞我的和親對象是個殘疾皇子呜笑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355