Java生产者-消费者-仓储 模型

Java生产者消费者模型


在工作中,理解并运用生产者消费者模型能在高并发开发中出现更少的问题,本文将从最从简而行,使用Java语言实现一个最简单的生产者消费者模型。


介绍

理解

现有如下需求,需要用Java实现以下操作流程:

  1. 多个蛋糕师生产蛋糕,多个消费者消费蛋糕;
  2. 蛋糕的最大仓库是固定的
  3. 蛋糕以先进先出原则出售,先生产的蛋糕先被出售,后生产的蛋糕后出售

若要实现这个过程,要借助Java线程的并发写作来做。

生产着消费者模型,完整称作生产者-消费者-仓储 模型

规则

  1. 仅当仓储未满时生产,仓储满则自动停止生产;
  2. 消费者仅在有商品时才能购买,仓空则等待;
  3. 消费者发现仓储无蛋糕时通知生产者生产;
  4. 生产者在生产出蛋糕后,通知等待的消费者去消费。
    附上一张图便于理解:
    仓库、生产者、消费者之间的关系

分析

Java中应该使用的核心API为:

  1. wait() 让当前线程进入阻塞状态,将这个线程存储到等待池中,并释放当前线程的所获得的锁
  2. norify() 唤醒等待池中的一个随机线程
  3. notifyAll() 唤醒等待池中所有线程

·注意:wait()/ notify() / notifyAll() 这三个方法必须被在同步代码中调用
·这些方法都是用于操作线程状态的,那么久必须要明确到底操作的是哪个锁上的线程


预备知识

Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从 wait()方法出返回,继续执行下面的操作。

  1. wait()
    该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用 wait()方法之后,当前线程会释放锁。如果调用 wait()方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException异常,这是以个 RuntimeException。如果再次获取到锁的话,当前线程才能从 wait()方法处成功返回。
  2. notify()
    该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
    该方法任意从 WAITTING 状态的线程中挑选一个进行通知,使得调用 wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用 wait()方法的线程能够从 wait()方法处退出。调用 notify 后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。
  3. notifyAll()
    该方法与 notify ()方法的工作方式相同,重要的一点差异是:
    notifyAll 使所有原来在该对象上 wait 的线程统统退出 WAITTING 状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。

代码实现

思路

  1. 定义仓库类
  • 实现生产和消费方法
  • 在仓满和仓空时执行等待 wait()、和唤醒 notify()
  • 使用同步锁
  1. 定义生产者线程类
  • 定义构造方法、初始化仓库类
  • 在*run()*函数中无限循环执行仓库类的生产方法
  1. 定义消费者线程类
  • 定义构造方法,初始化仓库类
  • run()函数中无限循环执行仓库类的*消费***方法

实现

  1. 新建一个仓库,用来存放产品,定义仓库最大值,另外开放两个函数,生产与消费,线程执行过快不便于观察,因此加入线程休眠
public class ProducerConsumer {
    class Warehouse // 仓库
    {
        // 存放商品的集合:由于遵循先进先出原则,因此优先使用LinkedList存放商品
        private LinkedList<Object> storeHouse = new LinkedList<>();
        private final static int MAX_NUM = 6; // 便于观察,仓库最大存放数量可以设置小一点

        public void produce() { // 生产,不断往storeHouse里存放商品
            while (storeHouse.size() == MAX_NUM) {
                System.out.println("库房已满,请生产者等待");
            }
            Object o = new Object();
            storeHouse.add(o);
            Thread.sleep((long) (Math.random() * 2000));
        }

        public void consume() { // 消费
            while (storeHouse.size() == 0) { // 不可以用if判断
                System.out.println("库存量为" + storeHouse.size() + ",请消费者等待,现在通知生产者去生产");
                lock.wait(); // 让当前状态处于等待
            }
            Object o = storeHouse.removeFirst();
            Thread.sleep((long) (Math.random() * 1000));
        }
    }
}
  1. 定义不断地生产、不断地消费的函数
    // 定义生产者线程类,不断地生产产品
    class Produce extends Thread {
        private Warehouse mWarehouse = null;

        public Produce(Warehouse wh) {
            this.mWarehouse = wh;
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                mWarehouse.produce();
            }
        }
    }

    // 定义消费者线程类,不断地生产产品
    class Customer extends Thread {
        private Warehouse mWarehouse = null;

        public Customer(Warehouse wh) {
            this.mWarehouse = wh;
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                mWarehouse.consume();
            }
        }
    }

执行以上代码:

public void excute() {
        Warehouse warehouse = new Warehouse();
        Produce p1 = new Produce(warehouse);
        p1.setName("生产者1号");
        p1.start();

        Produce p2 = new Produce(warehouse);
        p2.setName("生产者2号");
        p2.start();

        Customer c1 = new Customer(warehouse);
        c1.setName("消费者1号");
        c1.start();
        Customer c2 = new Customer(warehouse);
        c2.setName("消费者2号");
        c2.start();
        Customer c3 = new Customer(warehouse);
        c3.setName("消费者3号");
        c3.start();
    }
// 执行:
public static void main(String[] args) {
        ProducerConsumer p = new ProducerConsumer();
        p.excute();
    }

最终发现,由于没有加入同步代码锁,在执行过程中,经常会出现仓库为空时,几个消费者仍然会同时去仓库消费的情形,如此说来,加入同步锁势在必行。

  1. 修改
  • 在仓库新增一个对象锁
  • 在生产时,使用同步锁,当仓库满时,线程锁等待
  • 同样,在消费时使用线程锁,当仓库为空时,停止消费并等待
private Object lock = new Object();
public void produce() { // 生产,不断往storeHouse里存放商品
            try {
                synchronized (lock) {
                    while (storeHouse.size() == MAX_NUM) {
                        System.out.println("库房已满,请生产者等待 ");
                        lock.wait(); // 让生产者的线程处于等待中
                    }
                    Object o = new Object();
                    storeHouse.add(o);
                    System.out.println(Thread.currentThread().getName() + "商品 " + o.hashCode() + " 已入库,当前商品增加至" + storeHouse.size() + "个");
                    lock.notifyAll(); // 唤醒当前锁上处于被等待状态中的线程
                    Thread.sleep((long) (Math.random() * 3000));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

public void consume() { // 消费
            try {
                synchronized (lock) {
                    while (storeHouse.size() == 0) { // 不可以用if判断
                        System.out.println("库存量为" + storeHouse.size() + ",请消费者等待,现在通知生产者去生产");
                        lock.wait(); // 让当前状态处于等待
                    }
                    Object o = storeHouse.removeFirst();
                    System.out.println(Thread.currentThread().getName() + "商品 " + o.hashCode() + " 已消费,当前商品减少至" + storeHouse.size() + "个");
                    lock.notifyAll(); // 唤醒当前对象锁的等待状态
                    Thread.sleep((long) (Math.random() * 2000));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

ok,再次执行的结果就很美丽:

生产者2号商品 1058879343 已入库,当前商品增加至1个
生产者2号商品 836427516 已入库,当前商品增加至2个
生产者2号商品 904223775 已入库,当前商品增加至3个
生产者2号商品 1894241479 已入库,当前商品增加至4个
消费者1号商品 1058879343 已消费,当前商品减少至3个
消费者1号商品 836427516 已消费,当前商品减少至2个
消费者1号商品 904223775 已消费,当前商品减少至1个
消费者1号商品 1894241479 已消费,当前商品减少至0个
库存量为0,请消费者等待,现在通知生产者去生产

如此,一个最简单的Java生产者消费者模型就完成了。


总结

  • 生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。
    所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,比如案例中的仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。

  • 在 Object 提供的消息通知机制应该遵循如下这些条件:

不能使用if()语句判断循环中的条件,而应该使用 while 循环进行判断,避免早期通知以及等待条件发生改变的情况;
使用 NotifyAll 而不是使用 notify。


本文对应Demo地址下载连接


版权申明:未经作者同意,禁止转载。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值