消费者和生产者模式
是用来描述一个仓库(缓冲区),生产者可以将产品放入到仓库中,消费者可以从仓库中买走商品,解决生产者和消费者的生活逻辑问题,需要采用代码的同步机制来完成相互的【约束】和【提醒】。
注意事项
- 【商品是唯一共享资源】
- 消费者购买商品,清空商品的库存,要【提醒】生产者生产,并且消费者停止购买
- 生产者生产商品,填满商品的库存,要【提醒】消费者购买(到货通知),并且生产者是要停止生产操作
实现方法
采用wait()、notify()和notifyAll()方法。
wait():当缓冲区已满或空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等待状态,让其他线程执行
- 是Object的方法
- 调用方式:对象.wait();
- 表示释放 对象 这个锁标记,然后在锁外边等待(对比sleep(),sleep是抱着锁休眠的)
- 等待,必须放到同步代码段中执行
notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态
- 是Object的方法
- 调用方式:对象.notify();
- 表示唤醒 对象 所标记外边在等待的一个线程,或者从多个待唤醒线程中随机唤醒一个
notifyAll():全部唤醒
- 是Object的方法
- 调用方式:对象.notifyAll()
- 表示唤醒 对象 所标记外边等待的所有线程
案例
生产者生产面包,消费者消费面包,要求仓库有面包时,提醒消费者消费,生产者出于等待状态;当没有面包的时候,提醒生产者生产,消费者处于等待。即生产一件,消费一件。
基本思路:
- 创建面包类,构建相关的属性和方法。
- 创建面包仓库类,面包类对象传入,并创建sychronized修饰的同步方法——input和output方法。
- 创建两个线程,生产者product和消费者consumer,分别继承Runnable,重写run()方法,并分别调用input和output方法,保证同步。
- 在主方法创建Thread,并start。
/**
* 面包类
*/
public class Bread {
private int id;
private String productName;
public Bread() {
}
public Bread(int id, String productName) {
this.id = id;
this.productName = productName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
/**
* 面包容器类
*/
public class BreadCon {
private Bread con;
private boolean flag = false; //判断仓库是否有面包,false表示没有
public synchronized void input(Bread b) throws InterruptedException {
if (flag) {
this.wait(); //如果仓库有面包,则处于阻塞状态,等待消费者消费后唤醒
}
this.con = b;
System.out.println(Thread.currentThread().getName() + "生产了"
+ b.getId() + "号面包");
flag = true;
this.notify(); //仓库没有面包,则生产面包,并将标记改为true,唤醒消费者购买
}
public synchronized void output() throws InterruptedException {
if (!flag) {
this.wait();
}
Bread b= con;
con = null;
System.out.println(Thread.currentThread().getName()+"消费了"
+b.getId() + "号面包" +" 生产者:"+b.getProductName());
flag = false; //修改标记
this.notify(); //唤醒生产者生产
}
}
/**
* 生产者类
*/
public class Product implements Runnable{
private BreadCon con;
public Product(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
Bread b = new Bread(i, Thread.currentThread().getName());
try {
this.con.input(b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者类
*/
public class Consumer implements Runnable {
private BreadCon con;
public Consumer(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
try {
con.output();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 测试类
*/
public class test {
public static void main(String[] args) {
//创建容器
BreadCon con = new BreadCon();
//生产
Product product = new Product(con);
//消费
Consumer consumer = new Consumer(con);
//线程对象启动
new Thread(product, "面包生产商A").start();
new Thread(consumer, "消费者1").start();
}
}
//打印结果
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商A生产了3号面包
消费者1消费了3号面包 生产者:面包生产商A
面包生产商A生产了4号面包
消费者1消费了4号面包 生产者:面包生产商A
面包生产商A生产了5号面包
思考:如果生产方不止一个,消费方也不止一个,如何设计?
我们分别为生产者和消费者创建两个线程,运行结果如下:
new Thread(product, "面包生产商A").start();
new Thread(product, "面包生产商B").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();
//打印结果
//不止运行结果异常,而且还发生了死锁
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
消费者2消费了1号面包 生产者:面包生产商A
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
消费者2消费了2号面包 生产者:面包生产商A
为何会发生错误和死锁?
- 生产商A抢到CPU,执行生产面包,修改标记为true
- 生产商A抢到CPU,但是标记为true,等待,释放CPU和锁
- 生产商B抢到CPU,但是标记为true,等待,释放CPU和锁
- 消费者1抢到CPU,执行消费面包,修改标记为false,唤醒生产商A
- 生产商A抢到CPU,执行生产面包,修改标记为true,唤醒生产商B(notify随机唤醒一个)
- 生产商B抢到CPU,但是标记为true,等待,释放CPU和锁
- 生产商A抢到CPU,但是标记为true,等待,释放CPU和锁
- 消费者1抢到CPU,执行消费面包,修改标记为false,唤醒消费者2(notify随机唤醒一个)
- 消费者2抢到CPU,但是标记为false,等待,释放CPU和锁
- 消费者1抢到CPU,但是标记为false,等待,释放CPU和锁
最终四者全部处于阻塞状态,都在等待唤醒,发生死锁。
另外,当处于阻塞状态的线程被唤醒的时候,会继续执行后面代码。在第2、3步,A与B都被阻塞,在第4步,A被唤醒后,会继续执行后面生产面包的代码,然后在第5步,又抢到时间片,又会执行生产面包的代码,然后notify还唤醒了另一个生产商B,B会继续执行后面的生产面包代码,这样会出现多生产2次面包的情况,即错误状态。
解决办法:
- 将 if 判断语句改为 while 循环判断语句,保证被唤醒后,不再继续执行生产面包代码,而是返回进行循环判断判断
- 将 notify 改为 notifyAll ,避免发生死锁情况。
//这里只针对面包容器类进行修改
public class BreadCon {
private Bread con;
private boolean flag = false; //判断仓库是否有面包,false表示没有
public synchronized void input(Bread b) throws InterruptedException {
while (flag) {
this.wait();
}
this.con = b;
System.out.println(Thread.currentThread().getName() + "生产了" + b.getId()
+ "号面包");
flag = true;
this.notifyAll();
}
public synchronized void output() throws InterruptedException {
while (!flag) {
this.wait();
}
/*Bread b= con;
con = null;*/
System.out.println(Thread.currentThread().getName()+"消费了"+con.getId()
+ "号面包" +" 生产者:"+con.getProductName());
flag = false;
this.notifyAll();
}
}
//打印结果
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
面包生产商B生产了1号面包
消费者2消费了1号面包 生产者:面包生产商B
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商B生产了2号面包
消费者2消费了2号面包 生产者:面包生产商B
效率问题
虽然解决了错误和死锁问题,但如果生产者和消费者数量及其庞大,每次notifyAll会将无关的线程也唤醒,增加了无效的判断时间,效率大打折扣,所以这里采用Condition接口,配合Lock接口下的ReentrantLock实现类解决效率问题。
Condition接口
sychronized的锁方式,只能让生产者和消费者进入同一个阻塞队列,所以notifyAll会将队列中所有线程唤醒。
Condition接口则提供了两个队列,一个消费队列,一个生产队列,进行锁的微操,甚至可以自定义提供三个甚至更多队列,实现多线程的同步,并保证效率。
方法的使用
//创建Lock对象
Lock lock = new ReentrantLock();
//根据需求创建自定义数量的Condition条件队列
Condition condition = lock.newCondition();
//使某一队列等待,相当于wait()
condition.await();
//使某一队列唤醒,相当于notify()
condition.signal();
//
案例一:面包案例优化
这次我们将面包放到一个数组进行生产,当数组满了以后可以提醒消费者购买,所以也会在没满的时候,消费者也会购买,模拟实际的生产和消费模式。
//这里只提供核心代码块
public class BreadCon {
/*这里必须用-1,否则出现索引越界异常,因为后面消费者消费index--会出现index=-1的情况
*为了避免因为生产者生产的时候breads[index]调取异常
*会将index++放到赋值之前,保证索引从0开始
*/
private int index = -1;
private Bread[] breads = new Bread[5];
//创建ReentrantLock锁对象,并创建两个阻塞队列,一个是生产者proCon,一个是消费者conCon
private Lock lock = new ReentrantLock();
private Condition proCon = lock.newCondition();
private Condition conCon = lock.newCondition();
public BreadCon() {
}
public void input(Bread bread) {
lock.lock(); //上锁
try {
while (index > 3) {
proCon.await(); //如果面包数量超过数组容量,则生产者处于生产阻塞队列
}
index++; //因为Index是从-1开始,所以放到赋值之前,从0开始索引
breads[index] = bread;
System.out.println("生产者:" + Thread.currentThread().getName()
+ "生产了" + bread.getId() + "号面包");
conCon.signal(); //如果有面包,则将消费队列消费者唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
public void output() {
lock.lock();
try {
while (index < 0) {
try {
conCon.await(); //如果没有面包,则将消费者置于消费阻塞队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bread b = breads[index];
breads[index] = null;
System.out.println("消费者:" + Thread.currentThread().getName()
+ "消费了" + b.getId() + "号面包,生产者是:" + b.getProductName());
index--;
proCon.signal(); //每消费一次面包,可以唤醒生产者生产
} finally {
lock.unlock();
}
}
}
案例二:三个线程交替输出A B C ,输出20遍
这里需要构筑三个Condition队列锁。
public class PrintABC {
private int flag = 1; //1表示A, 2表示B, 3表示C
private Lock lock = new ReentrantLock();
private Condition aCon = lock.newCondition();
private Condition bCon = lock.newCondition();
private Condition cCon = lock.newCondition();
public void printA() {
lock.lock();
try {
while (1 != flag) {
//这里注意,必须将阻塞放到循环判断中
//这样最后一次循环的时候,A被唤醒,可以继续执行下面的代码,依次唤醒BC,线程退出
//否则,如果将后面代码放到判断里,将里面的阻塞放到外面,则A被唤醒,却不能调用signal()方法,导致B/C一直被阻塞,main线程退出,但是子线程还在,发生死锁,后面循环也不会继续
aCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
bCon.signal();
flag = 2;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (2 != flag) {
bCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
cCon.signal();
flag = 3;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (3 != flag) {
cCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
aCon.signal();
flag = 1;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//测试方法
public class test {
public static void main(String[] args) {
PrintABC printABC = new PrintABC();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
printABC.printA();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
printABC.printB();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
printABC.printC();
}
}
}, "C").start();
}
}