生产者、消费者模型是学习多线程的时候的一个很好的练习模型。该问题专业的说法应为:有限缓冲问题。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。
生产者的作用是生成一定量的数据放到缓冲区中,然后重复此过程。而消费者的作用则是消耗这些数据。问题的关键是要保证生产者不会在缓冲区满时加入数据,消费者也不会再缓冲区空时消耗数据。
解决问题的方法可以采用线程编程,也可以不采用线程编程。不采用的话就是简单的做满或空状态的判断,不满足条件的时候则放弃数据。当然这样的实现意义不大。如果要采用线程编程,则在缓冲区满时生产者休眠,等到消费者消耗缓冲区中的数据的时候再唤醒生产者。同理,在缓冲区空时让消费者休眠,当生产者往缓冲区添加数据之后再将其唤醒。
-------------------------------------------------------------------------------------------------------------------------------
以下的实现方案是自己学习了java线程知识之后写的一个初级解决方案,用到了wait(),notify()等基础线程通信方法。
称其实较为完善的方法,因为该方案可以实现单个生产者与单个消费者,也可以适用于多个生产者与多个消费者。
不仅如此,因为该方案中可以制定每个生产者生产任务,和每个消费者的消费任务,如果两者总和相等,那么非常容易解决。但是如果两者总和不等,比如生产者的总任务有20个,消费者的总任务有30个,那么生产者全部生产完之后,消费者将会无限期死等待。后来我加入了一些检测机制来解决死等待,如果一方任务已经完成,另外一方即将进入wait()的时候检测到了对方任务全部完成,则不再wait(),而是skip wait,也直接结束任务。
该实现的不足之处在于设计模式上的问题,因为没有好好地打草稿设计,可能一些方法的实现违背了OO的一些基本原则,留待以后改进。同时维基上提到了信号灯算法,管程算法等成熟的算法,以及多线程中condition、重入锁等特性都没有深入研究过,这些可以留待以后再实现。
--------------------------------------------------------------------------------------------------------------------------------
因为内容过多,只能简述一下实现思路:
为了比较好的模拟实际,我写了Consumer消费者类、Producer生产者类、Product产品类、Warehouse仓库类,已经对于单C单P,单C多P,多C单P,多C多P各写了一个测试类,结果运行良好。
其中几个类简述如下,具体看代码吧,内容太多了:
Consumer:模拟消费者,可以有多个实例,每个具有一个名字,每个的消费任务量可以指定,然后有一个类静态变量标明总任务数。
public class Consumer implements Runnable{
private static int totalTaskNumber=0;
private int myTaskNumber=0;
private static int totalTaskRemain=0;
private int myTaskRemain=0;
private String name;
private Warehouse wh;
private static int totalConsumed=0; //The total number of product produced yet.
private int thisOneConsumed=0; //The number of product produced by this producer.
private boolean needContinue=true;
public Consumer(String name, Warehouse wh, int taskNumber){
this.name=name;
this.wh=wh;
myTaskNumber=taskNumber;
myTaskRemain=taskNumber;
totalTaskNumber+=taskNumber;
totalTaskRemain+=taskNumber;
}
public void consume(){
// System.out.println("Im consumer here!");
Product popedProduct=wh.pop(this);
if(popedProduct==null) needContinue=false;
}
public void run(){
for(int i=0;i
Producer:模拟生产者,与消费者差不多。
public class Producer implements Runnable{
private static int totalTaskNumber=0;
private int myTaskNumber=0;
private static int totalTaskRemain=0;
private int myTaskRemain=0;
private String name;
private Warehouse wh;
private static int totalProduced=0; //The total number of product produced yet.
private int thisOneProduced=0; //The number of product produced by this producer.
private boolean needContinue=true;
public Producer(String name, Warehouse wh, int taskNumber){
this.name=name;
this.wh=wh;
myTaskNumber=taskNumber;
myTaskRemain=taskNumber;
totalTaskNumber+=myTaskNumber;
totalTaskRemain+=myTaskNumber;
}
public void produce(){
Product toProduce=new Product(++Product.totalID);
needContinue=wh.push(toProduce,this);
}
public void run(){
for(int i=0;i
Product:模拟生产的产品。现在这个类比较简单,只有一个属性ID编号,就是每次生产出来一个产品的时候,给它依次编个号。之所以写这个类来模拟生产的产品,而不是简单地推入数据,是因为以后可以方便地扩展该类,潜在的实用性还是很大的。
public class Product {
public static int totalID=0;
private int ID;
public Product(int ID){
this.ID=ID;
}
public int getID(){
return ID;
}
}
Warehouse:模拟仓库,也就是所谓的多线程共享的数据缓冲区,大部分设计心血都在这个类里。结构是用堆栈实现的,最重要的操作是push,pop方法,前者只能由生产者的实例调用,后者只能由消费者的实例调用。其他还有一些isEmpty,isFull等等的辅助方法,详情可以参见我博客里关于堆栈实现的一篇日志。
现在讲一下Warehouse里的一些细节,是实现的关键。当仓库满的时候,isFull方法返回true,这个时候再想调用push方法的生产者,他们想push的产品已经构造好了,但是在推入仓库的这一步将被转为wait(),直到有消费者来notifyAll()。同理,如果仓库满的时候,消费者再想pop()的话也将转入等待,直到被刚刚完成生产工作的生产者唤醒。进一步的,每一次生产任务完成后都要notifyAll,通知在等待的消费者可以消费了,每一次消费任务完成后也要用notifyAll来通知生产者仓库不再爆仓,可以来推入产品了。
接下来谈一下任务不同步的问题。每一个生产者或者消费者在被创建实例的时候都可以指派生产或者消费任务,如果生产者的总任务和与消费者的总任务和不相等的话,就会造成一方的永久等待。为了解决这个问题,加入生产者和消费者的类静态变量表明总任务,每生产或消费一个,静态变量减一,归零的时候表明这一方的任务全部完成了。故每一方在转入wait()方法的时候,先检查对方的这个变量,如果对方的这个变量是0,任务已经全部完成,那么你再等待也没意义了,只会陷入永久等待,因此这一方也就直接结束了。
如果是生产者先结束的话,那么消费者在将仓库里的产品消费光以后,即使还有消费任务也不管了,直接结束。
如果是消费者先结束的话,那么生产者其实会继续生产,将仓库里铺满爆仓,之后原本即将转入wait()状态,但是即使转入了也没有消费者来消费了,那么也就直接结束啦!
public class Warehouse {
private final int capacity=10;
private Product[] storage;
private int nextPos=0;
boolean allCsmFinished=false;
boolean allPdcFinished=false;
public Warehouse(){
storage=new Product[10];
}
//This method is only invoked by producer
public synchronized boolean push(Product toPush, Producer invoker){
//The use of while is very important. Using if() statement will cause exception in M_P_M_C model!.
while(isFull()){
try {
if(allCsmFinished){
System.out.println("ACF! "+invoker.getName()+" skips waiting!\n");
return false;
}
System.out.println(invoker.getName()+" waits!\n");
wait(); //The invoker of wait() method is acquiescently the owner of this synchronized lock.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage[nextPos]=toPush;
nextPos++;
System.out.println("Produced! ID "+toPush.getID()+" "+toString());
invoker.incThisOneProduced();
invoker.incTotalProduced();
System.out.println(invoker.toString()+"\n");
invoker.decMyTaskRemain();
invoker.decTotalTaskRemain();
invoker.testFinish();
if(invoker.getTotalTaskRemain()==0) setAllPdcFinished(true);
// System.out.println("Producer notifies all!\n");
//It's safe to notifyAll() at this moment since nextPos is already changed.
notifyAll();
return true;
}
//This method is only invoked by consumer
public synchronized Product pop(Consumer invoker){
//The use of while is very important. Using if() statement will cause exception in M_P_M_C model!.
while(isEmpty()){
try{
if(allPdcFinished){
System.out.println("APF! "+invoker.getName()+" skips waiting!\n");
return null;
}
System.out.println(invoker.getName()+" waits!\n");
// System.out.println(Test_SingleP_MultiC.CT1.isAlive());
// System.out.println(Test_SingleP_MultiC.CT2.isAlive());
// System.out.println(Test_SingleP_MultiC.PT1.isAlive());
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int idxToReturn=--nextPos;
System.out.println("Consumed! ID "+storage[idxToReturn].getID()+" "+toString());
invoker.incThisOneConsumed();
invoker.incTotalConsumed();
System.out.println(invoker.toString()+"\n");
invoker.decMyTaskRemain();
invoker.decTotalTaskRemain();
invoker.testFinish();
if(invoker.getTotalTaskRemain()==0) setAllCsmFinished(true);
//NotifyAll() at this time is secure, because nextPos is changed, and return index is saved in another variabl and thus unchanged.
notifyAll();
return storage[idxToReturn];
}
public boolean isEmpty(){
return nextPos==0;
}
public boolean isFull(){
return nextPos==capacity;
}
public int inventory(){
return nextPos;
}
public void setAllCsmFinished(boolean toSet){
allCsmFinished=toSet;
}
public void setAllPdcFinished(boolean toSet){
allPdcFinished=toSet;
}
public String toString(){
String str="";
str+="[Warehouse status: Inventory="+inventory()+" Products:";
for(int i=0;i
1.单生产者,单消费者。
public class Test_SingleP_SingleC {
public static void main(String[] args){
Warehouse wh=new Warehouse();
Thread PT1=new Thread(new Producer("P_one",wh,50),"ProducerThread one");
Thread CT1=new Thread(new Consumer("C_one",wh,50),"ConsumerrThread one");
// PT1.setPriority(Thread.MIN_PRIORITY);
// CT1.setPriority(Thread.MAX_PRIORITY);
PT1.start();
CT1.start();
}
}
2.单生产者,多消费者。
public class Test_SingleP_MultiC {
public static void main(String[] args){
Warehouse wh=new Warehouse();
Thread PT1=new Thread(new Producer("P_one",wh,50),"ProducerThread one");
Thread CT1=new Thread(new Consumer("C_one",wh,1),"ConsumerrThread one");
Thread CT2=new Thread(new Consumer("C_two",wh,1),"ConsumerrThread two");
// PT1.setPriority(Thread.MIN_PRIORITY);
// CT1.setPriority(Thread.MAX_PRIORITY);
PT1.start();
CT1.start();
CT2.start();
}
}
3.多生产者,单消费者。
public class Test_MultiP_SingleC {
public static void main(String[] args){
Warehouse wh=new Warehouse();
Thread PT1=new Thread(new Producer("P_one",wh,20),"ProducerThread one");
Thread PT2=new Thread(new Producer("P_two",wh,50),"ProducerThread two");
Thread CT2=new Thread(new Consumer("C_two",wh,30),"ConsumerrThread two");
// PT1.setPriority(Thread.MIN_PRIORITY);
// CT1.setPriority(Thread.MAX_PRIORITY);
PT1.start();
PT2.start();
CT2.start();
}
}
4.多生产者,多消费者。
public class Test_MultiP_MultiC {
public static void main(String[] args){
Warehouse wh=new Warehouse();
Thread PT1=new Thread(new Producer("P_one",wh,20),"ProducerThread one");
Thread PT2=new Thread(new Producer("P_two",wh,60),"ProducerThread two");
Thread CT1=new Thread(new Consumer("C_one",wh,50),"ConsumerrThread one");
Thread CT2=new Thread(new Consumer("C_two",wh,30),"ConsumerrThread two");
// PT1.setPriority(Thread.MIN_PRIORITY);
// CT1.setPriority(Thread.MAX_PRIORITY);
PT1.start();
PT2.start();
CT1.start();
CT2.start();
}
}
大概就解释到这里。考虑几点以后如果再实现的话要考虑的一些模式上的问题:
可不可以进一步分离功能,使每一个类的独立性更强,聚合性更少!
消费者与生产者只关心自己的任务,并将任务的进度向仓库类通知。
仓库类不关心消费生产的细节,只在接到通知后由两者的进度信息来做调度!
这样似乎更加符合实际……