一、问题描述
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。
示意图:
二、解决方法
思路
采用某种机制保护生产者和消费者之间的同步。有较高的效率,并且易于实现,代码的可控制性较好,属于常用的模式。
在生产者和消费者之间建立一个管道。管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。
解决问题的核心
保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
Java能实现的几种方法
- wait() / notify()方法
- await() / signal()方法
- BlockingQueue阻塞队列方法
- 信号量
- 管道
三、代码实现
1.wait() / notify()方法
当缓冲区已满时,生产者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行;
当缓冲区已空时,消费者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行。
当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;
当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
仓库Storage.java
import java.util.LinkedList;
/* 仓库 */
public class Storage {
/* 仓库容量 */
private final int MAX_SIZE = 10;
/* 仓库载体 */
private LinkedList<Object> list = new LinkedList<>();
public void produce(){
synchronized (list){
if(list.size() == MAX_SIZE){
System.out.println("[生产者" + Thread.currentThread().getName() + "] 仓库已满");
try{
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("[生产者" + Thread.currentThread().getName() + "生产1个产品] 现库存:" + list.size());
list.notifyAll();
}
}
public void consume(){
synchronized (list){
if(list.size() == 0){
System.out.println("[消费者" + Thread.currentThread().getName() + "] 仓库为空");
try{
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("[消费者" + Thread.currentThread().getName() + "消费1个产品] 现库存:" + list.size());
list.notifyAll();
}
}
}
生产者:
public class Producer implements Runnable {
/* 生产者 */
private String name;
private Storage storage;
public Producer(Storage storage, String name){
this.storage = storage;
this.name = name;
}
@Override
public void run() {
while (true){
try{
Thread.sleep((long) (5000*Math.random()));
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者:
public class Consumer implements Runnable {
private Storage storage;
private String name;
private Consumer(){}
public Consumer(Storage storage,String name){
this.storage = storage;
this.name = name;
}
@Override
public void run() {
while (true){
synchronized (storage){
try {
Thread.sleep((long) (1000*Math.random()));
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
p1-------------Thread-0
p2-------------Thread-1
p3-------------Thread-2
c1-------------Thread-3
c2-------------Thread-4
c3-------------Thread-5
[消费者Thread-3] 仓库为空
[生产者Thread-0生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[生产者Thread-2生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[消费者Thread-3] 仓库为空
[生产者Thread-2生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[生产者Thread-0生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[消费者Thread-3] 仓库为空
[生产者Thread-1生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[消费者Thread-3] 仓库为空
[生产者Thread-0生产1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
[生产者Thread-1生产1个产品] 现库存:1
[生产者Thread-2生产1个产品] 现库存:2
[生产者Thread-1生产1个产品] 现库存:3
[消费者Thread-3消费1个产品] 现库存:2
[消费者Thread-3消费1个产品] 现库存:1
[消费者Thread-3消费1个产品] 现库存:0
一个生产者线程运行produce方法,睡眠时间随机;一个消费者运行一次consume方法,睡眠随机,为了验证是否出现异常默认生产者平均速度小于消费者平均速度。此次实验过程中,有3个生产者和3个消费者,也就是我们说的多对多的情况。仓库的容量为10。
注意:
notifyAll()方法可使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,还是要看JVM的具体实现吧。