生产者消费者问题

这是一个面试经常被问到的问题,很多问题都可以转化为这个模型。

什么是生产者与消费者问题?举个例子,我们去吃自助餐,在自助餐的一个公共区域放着各种食物,消费者需要就自行挑选,当食物被挑没的时候,大家就等待,等候厨师做出更多再放到公共区域内供大家挑选;当公共区域食物达到一定数量,不能再存放的时候,此时没有消费者挑选,厨师此时等待,等公共区域有地方再存放食物时,再开始生产。这就是一个生产者与消费者问题。

根据这个例子,我们可以模拟一下场景,我们从这个例子中,显然看出我们需要制造一个公共区域,而且这个公共区域是有容量限制的,需要模拟各种食物,同时还需要模拟几个厨师也就是生产者,最后再模拟几个消费者。

首先呢,我们创建一个产品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();
    }
}

整个生产者和消费者就完成了。

这里面一定要思考,代码的同步,容器的唯一以及对容器容量的控制,之间是怎么进行通讯的。

Categories Go