昨天去了一家遊戲公司複試,這就是一道面試題目,要求用Java基礎實現生產者消費者模式(機試),當時準確地說只完成了一半。開啓兩個線程時沒什麼問題,但後來面試官要求開啓20個線程,結果就出現了假死。當時也沒弄懂是什麼原因導致假死,回來才弄懂!
1、什麼是生產者消費者模式?
在實際的開發工作中,也會有這樣的情節:某個模塊負責生產數據(產品),而這些數據由另一個模塊負責消費(此處的模塊是廣義的,可以是類、方法、線程、進程等)。在這裏,負責生產數據的模塊就是生產者,而負責處理這些數據的消費者,在生產者與消費者之間加一個數據的緩存區,生成着負責向其中放產品,而消費者從其中取產品,這樣就構成了生產者消費者模式。如圖:
2、生產者消費者模式的優點
(1)解耦
假設生產者和消費者分別是兩個類。如果讓生產者直接調用消費者的某個方法,那麼生產者對於消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會影響到生產者。而如果兩者都依賴於某個緩衝區,兩者之間不直接依賴,耦合也就相應降低了。
舉個例子,我們去郵局投遞信件,如果不使用郵筒(也就是緩衝區),你必須得把信直接交給郵遞員。有同學會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須得認識誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這就產生和你和郵遞員之間的依賴(相當於生產者和消費者的強耦合)。萬一哪天郵遞員換人了,你還要重新認識一下(相當於消費者變化導致修改生產者代碼)。而郵筒相對來說比較固定,你依賴它的成本就比較低(相當於和緩衝區之間的弱耦合)。
(2)支持併發
由於生產者與消費者是兩個獨立的併發體,他們之間是用緩衝區作爲橋樑連接,生產者只需要往緩衝區裏丟數據,就可以繼續生產下一個數據,而消費者只需要從緩衝區了拿數據即可,這樣就不會因爲彼此的處理速度而發生阻塞。
接上面的例子,如果我們不使用郵筒,我們就得在郵局等郵遞員,直到他回來,我們把信件交給他,這期間我們啥事兒都不能幹(也就是生產者阻塞),或者郵遞員得挨家挨戶問,誰要寄信(相當於消費者輪詢)。
(3)支持忙閒不均
緩衝區還有另一個好處。如果製造數據的速度時快時慢,緩衝區的好處就體現出來了。當數據製造快的時候,消費者來不及處理,未處理的數據可以暫時存在緩衝區中。等生產者的製造速度慢下來,消費者再慢慢處理掉。
爲了充分複用,我們再拿寄信的例子來說事。假設郵遞員一次只能帶走1000封信。萬一某次碰上情人節(也可能是聖誕節)送賀卡,需要寄出去的信超過1000封,這時候郵筒這個緩衝區就派上用場了。郵遞員把來不及帶走的信暫存在郵筒中,等下次過來時再拿走。
3、Java實現消費者生產者模式
(1)產品類
package com.producerconsumer.demo;
/**
*
* 產品類
* @author gerdan
*
*/
public class Product {
private String name;
public Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(2)產品銷售員
package com.producerconsumer.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 銷售員
* @author gerdan
*
*/
public class Clerk {
//貨架對象,用於存儲產品
private List<Product> store = new ArrayList<Product>();
//最多能存放數量
private static int MAX = 5;
/**
* 該方法由生產者調用
* 生產者把產品生產出來,然後調用該方法把產品交給售貨員
* @param product 商品
*/
public synchronized void receiveProduct(Product product){
if(store.size()>=MAX){
try {
// 目前沒有空間收產品,請稍候!
wait(); // 必須在同步塊或方法中才能使用 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.add(product);
System.out.printf("-->庫存狀態(%d) 新產品(%s)%n", store.size(), product.getName());
//通知等待區中的一個線程可以工作了
notify();
}
public synchronized Product buyProduct(){
if(store.size()==0){
try {
//目前貨架上沒有商品,需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Product p = store.remove(0);
System.out.printf("<--庫存狀態(%d) 取走產品(%s)%n", store.size(), p.getName());
// 通知等待區中的一個生產者可以繼續工作了
notify(); // notify 和 interrupt 會使等待區域的線程回到鎖定池的 Blocked 狀態
return p;
}
}
(3)生產者
package com.producerconsumer.demo;
/**
* 生產者
*
* @author gerdan
*
*/
public class Producer implements Runnable {
Clerk clerk = new Clerk();
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
// 暫停隨機時間
Thread.sleep((int) Math.random() * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product p = new Product(Thread.currentThread().getName()+"產品" + i);
//產品放入到貨架中
clerk.receiveProduct(p);
}
}
}
(4)消費者類
package com.producerconsumer.demo;
/**
* 消費者
* @author gerdan
*
*/
public class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
//暫停若干時間
Thread.sleep((int) Math.random() * 5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消費者取出商品
clerk.buyProduct();
}
}
}
(5)測試類
package com.producerconsumer.demo;
public class AppTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(producer,"GARDAN").start();
new Thread(consumer).start();
/* for (int i = 0; i < 10; i++) {
new Thread(producer).start();
new Thread(consumer).start();
}*/
}
}
以上代碼是我在機試時候寫,當時面試官說讓我開啓10個線程試試,開啓10個生產者和消費者線程,如測試類中註釋的部分。結果運行,就報錯了!
當時面試官就問我原因,一時間我也傻眼了。完全沒頭緒~~
後來回到家,查閱了一些資料才弄明白。故此需要mark下。
出錯原因在於銷售員類中,等待池有既有生產者線程也有消費者線程,在notify()調用時,無法確定喚醒的是生產者線程還是消費者線程,也就是說,當貨架已滿時,當前生產者線程wait()了,然後再調用notify(),而調用notify()是喚醒的生產者還是消費者是無法確定的,如果喚醒的是生產者,顯然再放入產品就會越界了。而相同的消費者取出商品時也是同一種情況。
找到原因後就要對症下藥,解決方案:
方案一:
修改銷售員類,如下:
package com.producerconsumer.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 銷售員
* @author gerdan
*
*/
public class Clerk {
//貨架對象,用於存儲產品
private List<Product> store = new ArrayList<Product>();
//最多能存放數量
private static int MAX = 5;
/**
* 該方法由生產者調用
* 生產者把產品生產出來,然後調用該方法把產品交給售貨員
* @param product 商品
*/
public synchronized void receiveProduct(Product product){
//使用while循環,判斷貨架是否已放滿,如果被放滿了就等待
while(store.size()>=MAX){
try {
// 目前沒有空間收產品,請稍候!
wait(); // 必須在同步塊或方法中才能使用 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.add(product);
System.out.printf("-->庫存狀態(%d) 新產品(%s)%n", store.size(), product.getName());
//喚醒等待區中的全部線程
notifyAll();
}
public synchronized Product buyProduct(){
//使用while循環,判斷貨架中是否有產品,如果沒有就繼續等待
while(store.size()==0){
try {
//目前貨架上沒有商品,需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Product p = store.remove(0);
System.out.printf("<--庫存狀態(%d) 取走產品(%s)%n", store.size(), p.getName());
// //喚醒等待區中的全部線程
notifyAll();
return p;
}
}
首先,修改receiveProduct()和buyProduct()中的notify()爲notifyAll()方法,喚醒等待池中全部線程。
然後,特別將等待條件有if判斷改爲while循環,以保證沒有未滿足條件時就持續等待。
以上解決方案性能比較低。第二種解決方案就是使用阻塞隊列,隊列是非常適合用於線程間的通訊。
方案二:
使用BlockingQueue阻塞隊列來實現,當產品放滿貨架時BlockingQueue就會阻塞,當產品被取完也會進入阻塞。
package com.producerconsumer.demo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerOptimize {
public static void main(String[] args) {
BlockingQueue<String> store = new ArrayBlockingQueue<String>(5);
ProducerQueue producer = new ProducerQueue(store);
ConsumerQueue consumer = new ConsumerQueue(store);
for (int i = 1; i <= 10; i++) {
new Thread(producer, "生產者" + i).start();
new Thread(consumer, "消費者" + i).start();
}
}
}
/**
* 生產者:產出產品放入到隊列queue中
*
* @author gerdan
*
*/
class ProducerQueue implements Runnable {
BlockingQueue<String> queue;
public ProducerQueue(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
// 停頓任意時間
Thread.sleep((int) Math.random() * 3000);
// 放入產品到隊列中
System.out.println(Thread.currentThread().getName() + ": 產出了產品" + i);
queue.put(Thread.currentThread().getName() + ": 產品" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消費者:從阻塞隊列中消費產品
*
* @author gerdan
*
*/
class ConsumerQueue implements Runnable {
BlockingQueue<String> queue;
public ConsumerQueue(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
// 停頓任意時間
Thread.sleep((int) Math.random() * 5000);
// 消費者消費產品
System.out.println(Thread.currentThread().getName() + ": 消費"
+ queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}