《生產(chǎn)者與消費者》

志梳理下,生產(chǎn)者消費者模式

簡單的模型

先從一個例子開始吧,有一些角色我先聲明如下:

  • 餐廳(Restaurant)--->載體
  • 廚師(Chef) --->生產(chǎn)者
  • 服務(wù)員(WaiterPerson) --->消費者
  • 食物(Meaf)--->被消費

我梳理一下它們的工作流程:

  • 故事的地點發(fā)生在餐廳,它是載體,包括了廚師、服務(wù)員、食物。

  • 廚師在餐廳做飯,做完飯,飯放在櫥窗,通知服務(wù)員端走,送給客人吃完;期間,廚師會不斷地監(jiān)控櫥窗的食物是否被端走,如果端走則繼續(xù)做新的食物,否則等待。

  • 服務(wù)員也不能閑著,它時刻留心著櫥窗是否有食物上架,如果沒有則繼續(xù)等待。如果有食物則端走,并通知廚師,我端走食物了,你可以做新食物了。

那么按照上面的步驟,首先我們看看生產(chǎn)者Chef的基本代碼

   synchronized(this){
       while (restaurant.meal != null) {
              wait(); 
       }
  }

上面代碼表示,倘若食物已經(jīng)做好一份了,廚師不斷監(jiān)控櫥窗上面的食物,如果沒有被服務(wù)員端走(消費),那我廚師就繼續(xù)等待,多休息一會。注意這里用while而不是if是因為防止多個消費者產(chǎn)生競爭引起并發(fā)問題。

 System.out.println("飯做好了,訂單生成...")。
  synchronized(restaurant.waiter){
       restaurant.meal=new Meal();
       restaurant.waiter.notifyAll();
 }

上面代碼表示 ,廚師沒有等待了,他開始做飯(生產(chǎn)),完成生產(chǎn)食物后,廚師通知(notifyAll)正在櫥窗等待食物的服務(wù)員,叫他去端菜(消費)。

那消費者Waiter的流程呢?我想過程應(yīng)該是和生產(chǎn)者恰好是對立的。

synchronized(this){
    while(restaurant.meal==null){
           wait();
      }
}

上面代碼表示,服務(wù)員不斷監(jiān)控櫥窗上面的食物有沒有做好,如果沒有做好,那我服務(wù)員就繼續(xù)等待。 是吧?和前面的生產(chǎn)者的判斷條件剛好對立。

 System.out.println("我服務(wù)員把飯端走了...")。
 synchronized(restaurant.chef){
       restaurant.meal=null;
       restaurant.chef.notifyAll();
}

上面代碼表示 ,服務(wù)員被廚師通知端飯(消費)了,于是他開始端飯送個客人,導(dǎo)致櫥窗上沒有飯了,之后,服務(wù)員通知(notifyAll)櫥窗口正在等待的廚師去做下一道菜(生產(chǎn))。

通過上面的例子,我們可以初步了解生產(chǎn)者與消費者的工作模式。但是實際開發(fā)場景中,應(yīng)該有不止一個生產(chǎn)者或者消費者,而且食物應(yīng)該很多,那么這個時候我們應(yīng)該引入隊列(Queque)這個數(shù)據(jù)結(jié)構(gòu)來管理它們了。

利用隊列管理生產(chǎn)者與消費者

我們可以設(shè)想一下,在餐廳中的業(yè)務(wù)場景,廚師chef應(yīng)該作為Runable角色可以有多個,我們可以用Excutor.submit(r)提交很多個廚師,讓其工作, 而服務(wù)員我們也可以有多個,同理,我們也把他放入線程池去運行。 而食物Meal也有多個,并且我們要用一個數(shù)據(jù)結(jié)構(gòu)存取它,讓它作為廚師和服務(wù)員兩者共同占有的資源又能做好同步處理。在上面的例子中,我們用wait(),notifyAll(),synchronized等方法進行食物的同步與通信。它們有一個明顯的缺點,我們發(fā)現(xiàn)代碼很是耦合,晦澀難懂,暫且不談性能。

讓開發(fā)者欣慰的事,JDK中提供了BlockQueque接口來存取“食物”。它是一個阻塞隊列的數(shù)據(jù)結(jié)構(gòu)。在這里,我們需要了解兩點;

  • 在開發(fā)過程中,"食物"常常指是的IO流。如網(wǎng)絡(luò)編程中,服務(wù)端與客戶端發(fā)送字節(jié)流相互通信。現(xiàn)在有netty或者nio等異步非阻塞IO的框架,讓并發(fā)性能更佳。

  • 阻塞是為了保證生產(chǎn)者與消費者步調(diào)一致,不要產(chǎn)生大量浪費的食物,消費者吃不完,導(dǎo)致資源耗盡。亦或者消費者盲目的去找生產(chǎn)者要食物,太多消費者擁擠,也會消耗資源。所以在剛剛開始的時候,jdk做了這個BlockQueque來管理食物和生產(chǎn)者和消費者通信。 生產(chǎn)者要生產(chǎn)食物如下面的代碼:

    @Override
      public void run() {
      try {
          while (!Thread.interrupted()) {
              Meal meal = new Meal(++count);
              mBlockQueque.put(meal);// 如果mBlockQueque容量不為empty則阻塞等待。                                             
              TimeUnit.SECONDS.sleep(2);//模擬生產(chǎn)耗時任務(wù)。
           }
        } catch (InterruptedException e) {
          System.out.println("Chef sleep end interrupted...");
          e.printStackTrace();
        }
    }
    

上面mBlockQueque.put()為阻塞方法(如果櫥窗(隊列)還有食物未被領(lǐng)取,則等待不生產(chǎn)食物,否則生產(chǎn)食物并添加至櫥窗),如注釋上的說明,它的作用類似wait()/add();我們跟蹤下源碼:

   /**
     * @throws NullPointerException {@inheritDoc}
     * @throws InterruptedException {@inheritDoc}
     */
    public void putFirst(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock(); //                                            ---(1)
        try {
            while (!linkFirst(node))
                notFull.await();                                   ---(2)
        } finally {
            lock.unlock();
        }
    }

我解析下上面的代碼:put()是一個接口方法,它具體的實現(xiàn)方法之一是putFirst,給鏈表首位添加一個元素。

  1. (1)此處有l(wèi)ock-finally-unlock組成的臨界區(qū)。它的作用類似synchronized,用來同步。它們之間不同的地方是:

一、用synchronized聲明鎖時,任務(wù)A和任務(wù)B,都要獲取鎖O,如果A首先獲得鎖O,B則一直等待直到A釋放鎖,B一直阻塞著不能被中斷。
二、用lock-finally-unlock聲明鎖時,任務(wù)A和任務(wù)B,都要獲取鎖O,如果A首先獲得鎖O,B可以等待一段時間,不想等待了,可以自行中斷。A如果想釋放鎖必須在finally后調(diào)用unlock。所以說我覺得lock更加靈活。
但是在大多數(shù)資源競爭不太激烈的情況下,我們還是用synchronized足夠了。

  1. (2)此處notFull是Condition的實例。它提供更好的性能,通過await()/signal()方法扮演之前的wait()/notify()的角色。 這里代碼是指while判斷鏈表是否超過容量,返回false時,則調(diào)用await()阻塞等待當(dāng)前任務(wù)線程。

我們分析完生產(chǎn)者chef,我們來看看消費者waiter的改造后的代碼:

@Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                Meal meal = mBlockQueque.take();//從隊列中remove出一個食物,沒有食物則阻塞等待
                meal.run();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

上述代碼中,mBlockQueque.take()是一個可阻塞方法。它試圖從櫥窗隊列上取食物,如果發(fā)現(xiàn)沒有食物就阻塞消費者線程。看看take()的具體實現(xiàn)的源碼:

 public E takeFirst() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ( (x = unlinkFirst()) == null) //                       ---(1)
                notEmpty.await();                                         ---(2)
            return x;
        } finally {
            lock.unlock();
        }
    }

(1)takeFisrt()方法中會有去調(diào)用unlinkFirst()去隊列返回一個食物,如果有食物,就返回,并調(diào)用notFull.signal()喚醒正在阻塞的生產(chǎn)者線程。
(2) notEmpty是另外一個Condition實例,它用來和消費者線程通信。如果發(fā)現(xiàn)返回的食物為空,則notEmpty.await()讓消費者線程阻塞等待。
至此。我們看到我們把具體的通信交互過程封裝到了阻塞隊列BlockQueue里。 生產(chǎn)者只需要調(diào)用take通信,消費者只需調(diào)用put通信。如下圖:

通信結(jié)構(gòu)

寫到這里了,那生產(chǎn)者與消費者模式有哪些實際應(yīng)用呢? 我想線程池應(yīng)該是應(yīng)用最廣泛的地方。下一篇我將詳細介紹線程池的原理。

注:部分參考自《Java 編程思想》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內(nèi)容