Java多線程與併發應用-(3)-傳統線程通信技術及生產者消費者模式

生產者/消費者問題是一個經典的線程同步以及通信的案例。該問題描述了兩個共享固定大小緩衝區的線程,即所謂的“生產者”和“消費者”在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,再喚醒消費者,通常採用線程間通信的方法解決該問題。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。

例:

寫一個線程同步通信的例子,生產者消費者模式,廚師做熱狗,售貨員賣熱狗, 有一個盒子,最多可以放10個熱狗,盤子沒有熱狗時,售貨員休息,盤子中有10個熱狗時,廚師休息,0<count<10時,2個廚師,2個售貨員同時幹活

代碼如下:

package com.lipeng.waitnotify;

/**
 * 熱狗類
 * @author LP
 *
 */
public class HotDog {
	
}

package com.lipeng.waitnotify;

import java.util.ArrayList;
import java.util.List;

/**
 * 盛放熱狗的盒子
 * 功能:數據緩存區,容量爲10
 * 說明:將不同線程(生產者和消費者)對數據的處理放在同一個類(數據緩存區)中,
 * 這樣要同步的方法就可以通過共享數據(此例中的list和count(即list.size))的臨界條件來決定執行哪個方法
 * @author LP
 *
 */
public class Box {
	private List<HotDog> list;//存放的熱狗
	private int count;//當前存放熱狗的數量
	public Box()
	{
		
	}
	public List<HotDog> getList() {
		return list;
	}
	public void setList(List<HotDog> list) {
		this.list = list;
	}
	public int getCount() {
		return count;
	}
	public void setCount(int count) {
		this.count = count;
	}
	/**
	 * 生產
	 * lipeng
	 * 2015-4-13
	 */
	public synchronized void cook(String cookerName)throws Exception
	{
		//當盒子中總數爲10時,廚師休息
		while(count==10)
		{
			this.wait();
		}
		HotDog hotDog=new HotDog();
		if(list==null)
		{
			list=new ArrayList<HotDog>();
		}
		list.add(hotDog);
		count++;
		System.out.println(cookerName+" 做了一個熱狗,盤子總一共-----------   "+count);
		this.notifyAll();//喚醒其他線程,cook或sale
	}
	/**
	 * 銷售
	 * lipeng
	 * 2015-4-13
	 */
	public synchronized void sale(String salerName)throws Exception
	{
		//當盒子中沒有熱狗時,售貨員休息。
		while(count==0)
		{
			this.wait();
		}
		list.remove(0);
		count--;
		System.out.println(salerName+" 賣出去一個熱狗,盤子總一共---------------------------------------------------------------   "+count);
		this.notifyAll();//喚醒其他線程,cook或sale
	}
}

package com.lipeng.waitnotify;

/**
 * 廚師類
 * 功能:做熱狗,生產者
 * @author LP
 *
 */
public class Cooker implements Runnable{
	private String name;
	private Box box;
	public String getName() {
		return name;
	}
	public Cooker(String name,Box box) {
		this.name=name;
		this.box=box;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Box getBox() {
		return box;
	}
	public void setBox(Box box) {
		this.box = box;
	}
	@Override
	public void run() {
		try {
			for(int i=0;i<200;i++)
			{
				box.cook(name);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

package com.lipeng.waitnotify;

/**
 * 售貨員類
 * 功能:賣熱狗,消費者
 * @author LP
 *
 */
public class Salesperson implements Runnable{
	private String name;
	private Box box;
	public String getName() {
		return name;
	}
	public Salesperson(String name,Box box) {
		this.name=name;
		this.box=box;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Box getBox() {
		return box;
	}
	public void setBox(Box box) {
		this.box = box;
	}
	@Override
	public void run() {
		try {
			for(int i=0;i<200;i++)
			{
				box.sale(name);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

package com.lipeng.waitnotify;


public class Main {
	public static void main(String[] args) {
		Box box=new Box();
		Cooker cooker1=new Cooker("老張",box);
		Cooker cooker2=new Cooker("老王",box);
		Salesperson saler1=new Salesperson("小李", box);
		Salesperson saler2=new Salesperson("小劉", box);
		new Thread(cooker1).start();
		new Thread(cooker2).start();
		new Thread(saler1).start();
		new Thread(saler2).start();
	}
}

運行結果:

說明

1. 需要同步的對象?即共享的數據是Box,那麼就要對Box加同步鎖,也必須調用box的wait和notify方法。

2. wait和notify方法必須在synchronized方法或塊中使用否則,拋出IllegalMonitorStateException異常。

3.要用到共同數據(包括同步鎖)的若干個方法應該歸在同一個類(數據緩存區)上,同步方法所處理的共享數據是這個類的一個屬性,這種設計正好體現了高類聚和程序的健壯性。要同步的方法之間用共享的變量做控制

4. 注意外層循環,即調用實際業務cook和sale的循環,應該在線程的run方法中,而不是在主方法中開啓多個線程。這是兩種不同的情況。一個是一個線程執行n次,另一個是n個線程執行1次。

5. 爲什麼要用while而不用if  在wait方法說明中,也推薦使用while,因爲在某些特定的情況下,線程有可能被假喚醒,使用while會循環檢測更穩妥,下個例子中做詳細解釋。

6. 當有大於兩個線程在執行時最好使用notifyAll喚醒全部阻塞線程。爲什麼,下篇文章詳解。


部分內容轉自:http://blog.csdn.net/ghsau/article/details/7433673

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