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