这是一个面试经常被问到的问题,很多问题都可以转化为这个模型。
什么是生产者与消费者问题?举个例子,我们去吃自助餐,在自助餐的一个公共区域放着各种食物,消费者需要就自行挑选,当食物被挑没的时候,大家就等待,等候厨师做出更多再放到公共区域内供大家挑选;当公共区域食物达到一定数量,不能再存放的时候,此时没有消费者挑选,厨师此时等待,等公共区域有地方再存放食物时,再开始生产。这就是一个生产者与消费者问题。
根据这个例子,我们可以模拟一下场景,我们从这个例子中,显然看出我们需要制造一个公共区域,而且这个公共区域是有容量限制的,需要模拟各种食物,同时还需要模拟几个厨师也就是生产者,最后再模拟几个消费者。
首先呢,我们创建一个产品Product类,这个类就代表食物的模板,厨师们就生产这种类型的食物,类里面定义食物的ID和name这两个属性,代码如下:
public class Product { private int id; private String name; public Product(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "Product :id:"+id+",name:"+name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
接下来,我们创建那个公共区域,也就是一个容器,容器要么用数组保存,要么用集合,这里我们用集合LinkedList,然后呢,我们要清楚我们这个容器只能被创建一个,也就是公共区域只有一个,我们放也是放在这里面,取也是从这里面取,这里采用单例模式来保证只能创建一个容器实例,容器里面再定义放和取的方法,我们要加锁保证放和取的同步,以免发生线程安全问题,同时还要定义容器的最大容量,再放和取的同步方法里面,我们要判断是否容器内的食物超出了容器的最大容量,如果放的时候大于等于最大容量了,就不能再往里面放了,这时候厨师们(生产者)要等待;取的时候也一样,看是否容器内的食物个数为0,为0消费者们就要等待,最后我们在容器里面定义一个检查容器容量的方法,后面单独开启一个线程调用此方法,实时监测容器内食物的个数,一直在0-10之间,就说明我们写的代码没有问题,代码如下:
public class Container { //单例模式,保证只能创建一个容器实例 private static Container instance = null; private Container(){} public static Container getInstance(){ if(instance == null){ instance = new Container(); } return instance; } private LinkedList<Product> list = new LinkedList<>();//容器 private int MAX = 10;//容器的最大容量 //放食物 public synchronized void putProduct(Product product){ while(list.size()>=MAX){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.addLast(product);//把新生产的食物放在最后 notifyAll(); } //取食物 public synchronized Product getProduct(){ while(list.size()<=0){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } notifyAll(); return list.removeFirst();//先取出放在这里时间最长的那个 } //检查容器内食物个数 public int checkSize(){ return list.size(); } }
接下来就是生产者(厨师)线程,这个线程里面就是不停的生产食物,然后放入容器里面供消费者挑选,更形象一点,生产食物需要固定的时间,我们让线程每生产一个食物睡眠一段时间,时间为固定的。
代码如下:
public class ProducerThread implements Runnable{ Container container = null; public ProducerThread(Container container) { this.container = container; } @Override public void run() { int i = 0; while(true){ Product product = new Product(++i, "产品"+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } container.putProduct(product); } } }
有了生产者,接下来就是消费者线程了,该线程里面消费者不停的挑选产品,消费产品,每消费一个食物也需要一定的时间,而且这个对于每一个人时间是不一定的,所以呢我们就取1200以内的随机数,进行睡眠。
代码如下:
public class CustomThread implements Runnable{ Container container = null; public CustomThread(Container container) { this.container = container; } @Override public void run() { while(true){ Product p = container.getProduct(); try { Thread.sleep((int)(Math.random()*1200)); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("消费了一个产品"+p); } } }
在生产者与消费者线程里面,我们都定义了构造器,用来接收容器对象,这里面,并没有对容器进行初始化,就是为了保证容器的唯一性。
所需要的我们都定义完毕,最后定义一个测试类Go,测试类里面,创建2个生产者线程,10个消费者线程,并创建一个容器传给生产者和消费者,同时,另外开启一个线程,每隔300毫秒进行输出容器内食物的数量,如果食物数量一直在0到10之间,说明我们的程序没有问题,为了让打印看的更清晰,这里不用System.out.println打印,而是用显示为红色的System.err.println进行输出。
代码如下:
public class Go { public static void main(String[] args) { Container container = Container.getInstance(); for (int i = 0; i < 2; i++) { new Thread(new ProducerThread(container)).start();; } for (int i = 0; i < 10; i++) { new Thread(new CustomThread(container)).start();; } new Thread(new Runnable() { @Override public void run() { while(true){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.err.println(container.checkSize()); } } }).start(); } }
整个生产者和消费者就完成了。
这里面一定要思考,代码的同步,容器的唯一以及对容器容量的控制,之间是怎么进行通讯的。