淺析生產者消費者模式--多線程假死

昨天去了一家遊戲公司複試,這就是一道面試題目,要求用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();
			}
		}
	}
}






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章