Java生产者消费者模型
在工作中,理解并运用生产者消费者模型能在高并发开发中出现更少的问题,本文将从最从简而行,使用Java语言实现一个最简单的生产者消费者模型。
介绍
理解
现有如下需求,需要用Java实现以下操作流程:
- 多个蛋糕师生产蛋糕,多个消费者消费蛋糕;
- 蛋糕的最大仓库是固定的
- 蛋糕以先进先出原则出售,先生产的蛋糕先被出售,后生产的蛋糕后出售
若要实现这个过程,要借助Java线程的并发写作来做。
生产着消费者模型,完整称作生产者-消费者-仓储 模型
规则
- 仅当仓储未满时生产,仓储满则自动停止生产;
- 消费者仅在有商品时才能购买,仓空则等待;
- 消费者发现仓储无蛋糕时通知生产者生产;
- 生产者在生产出蛋糕后,通知等待的消费者去消费。
附上一张图便于理解:
分析
Java中应该使用的核心API为:
- wait() 让当前线程进入阻塞状态,将这个线程存储到等待池中,并释放当前线程的所获得的锁
- norify() 唤醒等待池中的一个随机线程
- notifyAll() 唤醒等待池中所有线程
·注意:wait()/ notify() / notifyAll() 这三个方法必须被在同步代码中调用
·这些方法都是用于操作线程状态的,那么久必须要明确到底操作的是哪个锁上的线程
预备知识
Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从 wait()方法出返回,继续执行下面的操作。
- wait()
该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用 wait()方法之后,当前线程会释放锁。如果调用 wait()方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException异常,这是以个 RuntimeException。如果再次获取到锁的话,当前线程才能从 wait()方法处成功返回。 - notify()
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
该方法任意从 WAITTING 状态的线程中挑选一个进行通知,使得调用 wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用 wait()方法的线程能够从 wait()方法处退出。调用 notify 后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。 - notifyAll()
该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出 WAITTING 状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。
代码实现
思路
- 定义仓库类
- 实现生产和消费方法
- 在仓满和仓空时执行等待 wait()、和唤醒 notify()
- 使用同步锁
- 定义生产者线程类
- 定义构造方法、初始化仓库类
- 在*run()*函数中无限循环执行仓库类的生产方法
- 定义消费者线程类
- 定义构造方法,初始化仓库类
- 在run()函数中无限循环执行仓库类的*消费***方法
实现
- 新建一个仓库,用来存放产品,定义仓库最大值,另外开放两个函数,生产与消费,线程执行过快不便于观察,因此加入线程休眠
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));
}
}
}
- 定义不断地生产、不断地消费的函数
// 定义生产者线程类,不断地生产产品
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();
}
最终发现,由于没有加入同步代码锁,在执行过程中,经常会出现仓库为空时,几个消费者仍然会同时去仓库消费的情形,如此说来,加入同步锁势在必行。
- 修改
- 在仓库新增一个对象锁
- 在生产时,使用同步锁,当仓库满时,线程锁等待
- 同样,在消费时使用线程锁,当仓库为空时,停止消费并等待
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地址下载连接
版权申明:未经作者同意,禁止转载。