什么是生产者消费者问题?
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
代码实现
1、使用synchronized实现:
public class ProducerConsumer {
public static void main(String[] args) {
Commodity commodity = new Commodity();
//创建一个生产者线程,生产10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"生产者1").start();
//创建一个消费者线程,消费10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者1").start();
}
}
//商品类
class Commodity{
//商品数量
private int commodityNum = 0;
//商品增加方法(相当于生产者)
public synchronized void increment() throws InterruptedException {
if(commodityNum != 0){//判断商品数量是否为0
this.wait();//要是不为0,生产者等待
}
commodityNum++;//生产一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
this.notifyAll();//生产完唤醒其他线程
}
//商品减少方法(相当于消费者)
public synchronized void decrement() throws InterruptedException {
if(commodityNum == 0){//判断商品数量是否为0
this.wait();//要是为0,消费者等待
}
commodityNum--;//消费一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
this.notifyAll();//消费完唤醒其他线程
}
}
运行结果:
2、存在的问题
上面的程序看来好像没什么问题,生产者与消费者轮换执行,程序按照我们预想的执行完毕
但是,让我们多加入几个线程看看。
public class ProducerConsumer {
public static void main(String[] args) {
Commodity commodity = new Commodity();
//创建一个生产者线程,生产10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"生产者1").start();
//创建一个消费者线程,消费10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者1").start();
//创建一个生产者线程,生产10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"生产者2").start();
//创建一个消费者线程,消费10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者2").start();
}
}
//商品类
class Commodity{
//商品数量
private int commodityNum = 0;
//商品增加方法(相当于生产者)
public synchronized void increment() throws InterruptedException {
if(commodityNum != 0){//判断商品数量是否为0
this.wait();//要是不为0,生产者等待
}
commodityNum++;//生产一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
this.notifyAll();//生产完唤醒其他线程
}
//商品减少方法(相当于消费者)
public synchronized void decrement() throws InterruptedException {
if(commodityNum == 0){//判断商品数量是否为0
this.wait();//要是为0,消费者等待
}
commodityNum--;//消费一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
this.notifyAll();//消费完唤醒其他线程
}
}
运行结果:
预想中在商品数量不等于0的情况下生产者线程应该等待
但是,可以看到在多个生产者消费者线程的情况下出现了商品数量不等于0生产者线程还在生产商品的情况,这就是我们要说的问题虚假唤醒
在还有商品的情况下,生产者线程1进入判断,判断完后线程等待,此时另一条生产者线程2拿到执行权同样进入判断然后等待,此时要是消费者线程消费一个商品后唤醒其他线程,生产者线程1与生产者线程2都会从判断内开始执行,不会再进行判断,两个线程同时生产商品,就会造成商品数量大于1,这就是虚假唤醒
3、解决方案
将if判断改为while循环即可,其他不用变化
这样下一次线程在while循环内被唤醒还是要判断商品数量,即可避免虚假唤醒问题
4、condition简单介绍
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
Condition是个接口,基本的方法就是await()和signal()方法;Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition();调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock. Lock()和lock.unlock之间才可以使用
- Condition中的await()对应Object的wait();
- Condition中的signal()对应Object的notify();
- Condition中的signalAll()对应Object的notifyAll()。
5、使用lock实现:
public class ProducerConsumer {
public static void main(String[] args) {
Commodity commodity = new Commodity();
//创建一个生产者线程,生产10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"生产者1").start();
//创建一个消费者线程,消费10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者1").start();
//创建一个生产者线程,生产10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"生产者2").start();
//创建一个消费者线程,消费10件商品
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
commodity.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者2").start();
}
}
//商品类
class Commodity{
//商品数量
private int commodityNum = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); // 等待
//condition.signalAll(); // 唤醒全部
//商品增加方法(相当于生产者)
public void increment() throws InterruptedException {
lock.lock();
try {
while (commodityNum != 0){//判断商品数量是否为0
condition.await();//要是不为0,生产者等待
}
commodityNum++;//生产一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
condition.signalAll();//生产完唤醒其他线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//商品减少方法(相当于消费者)
public void decrement() throws InterruptedException {
lock.lock();
try {
while (commodityNum == 0){//判断商品数量是否为0
condition.await();//要是为0,消费者等待
}
commodityNum--;//消费一个商品
System.out.println(Thread.currentThread().getName()+"生产了一件商品,商品数量为"+commodityNum+"件");
condition.signalAll();//消费完唤醒其他线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
参考: