■ 什么是生产者/消费模式
在工作中,大家可能会碰到这样一种情况:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。在生产者与消费者之间在加个缓冲区,我们形象的称之为仓库,生产者负责往仓库了进商品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模式。
打个通俗的例子,就好比你写信,放入邮筒,取件员取走信这一些过程。你写信,就是生产的过程,而你将信放入邮筒则是将数据存入缓冲区,而快递员从邮筒取走你的信,相当于消费者从缓冲区将你的数据消费了。
■ 使用生产者/消费模式的好处
借用这篇文章所讲的,说的挺好的,感兴趣的同学可以看看:网页链接
1.解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
接着上述的例子,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
2.支持并发
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面的帖子会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
其实当初这个模式,主要就是用来处理并发问题的。
从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,都挺土的。
3.支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
■ 三种代码实现
1.使用synchronized和wait()、notifyAll()
使用wait()让当前线程等待并释放锁,让notifyAll()唤醒沉睡的线程让它继续执行
public static void main(String[] args) {
Resource resource = new Resource();
//生产者线程
Producer p1 = new Producer(resource);
Producer p2 = new Producer(resource);
Producer p3 = new Producer(resource);
//消费者线程
Consumer c1 = new Consumer(resource);
Consumer c2 = new Consumer(resource);
Consumer c3 = new Consumer(resource);
//启动生产
p1.start();
p2.start();
p3.start();
//启动消费
c1.start();
//c2.start();
//c3.start();
}
static class Resource {
//库存拥有数量 volatile关键字修饰
private volatile int num = 0;
//仓库最大容量
private int size = 10;
public synchronized void remove() {
if (num > 0 && num <= size) {
num--;
notifyAll(); //通知生产者制造资源
System.out.println("消费者:" + Thread.currentThread().getName() + "消耗了一件资源," + "当前库存还有:" + num + "个");
} else {
try {
System.out.println("消费者:" + Thread.currentThread().getName() + "线程即将进入等待状态...");
//如果没有资源,则使消费者进入等待状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void add() {
//仓库未满
if (num < size && num >= 0) {
num++;
notifyAll(); // 通知等待的消费者
System.out.println("生产者:"+Thread.currentThread().getName() + "生产了一件资源,当前库存有:" + num + "个");
} else {
try {
System.out.println("生产者:"+Thread.currentThread().getName() + "线程即将进入等待状态...");
//仓库满了,等待消费者拿出库存
wait();
//创建 就绪 阻塞 运行 死亡
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程
static class Consumer extends Thread {
private Resource resource1;
public Consumer(Resource resource) {
resource1 = resource;
}
@Override
public void run() {
//消费者不断消费
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource1.remove(); //消费者取出库存
}
}
}
//生产者线程
static class Producer extends Thread {
private Resource resource1;
public Producer(Resource resource) {
resource1 = resource;
}
@Override
public void run() {
//生产者不断生产
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource1.add(); //生产者添加库存
}
}
}
运行结果,当然你们的可能和我不一样👇:
生产者:Thread-1生产了一件资源,当前库存有:1个
生产者:Thread-0生产了一件资源,当前库存有:2个
消费者:Thread-3消耗了一件资源,当前库存还有:1个
生产者:Thread-2生产了一件资源,当前库存有:2个
生产者:Thread-0生产了一件资源,当前库存有:3个
生产者:Thread-2生产了一件资源,当前库存有:4个
生产者:Thread-1生产了一件资源,当前库存有:5个
消费者:Thread-3消耗了一件资源,当前库存还有:4个
生产者:Thread-2生产了一件资源,当前库存有:5个
生产者:Thread-0生产了一件资源,当前库存有:6个
生产者:Thread-1生产了一件资源,当前库存有:7个
消费者:Thread-3消耗了一件资源,当前库存还有:6个
消费者:Thread-3消耗了一件资源,当前库存还有:5个
生产者:Thread-1生产了一件资源,当前库存有:6个
生产者:Thread-0生产了一件资源,当前库存有:7个
生产者:Thread-2生产了一件资源,当前库存有:8个
生产者:Thread-1生产了一件资源,当前库存有:9个
消费者:Thread-3消耗了一件资源,当前库存还有:8个
生产者:Thread-2生产了一件资源,当前库存有:9个
生产者:Thread-0生产了一件资源,当前库存有:10个
消费者:Thread-3消耗了一件资源,当前库存还有:9个
生产者:Thread-0生产了一件资源,当前库存有:10个
生产者:Thread-2线程即将进入等待状态…
生产者:Thread-1线程即将进入等待状态…
消费者:Thread-3消耗了一件资源,当前库存还有:9个
生产者:Thread-0生产了一件资源,当前库存有:10个
生产者:Thread-2线程即将进入等待状态…
生产者:Thread-0线程即将进入等待状态…
生产者:Thread-1线程即将进入等待状态…
消费者:Thread-3消耗了一件资源,当前库存还有:9个
2.使用Lock和Condiction的await()、signalAll()
如果不太懂Lock和Condiction得使用方法,可以看一下这篇文章:网页链接
注意要在finally后面使用lock.unlock()
释放锁,需手动释放。
// 使用Lock 和 Condition解决生产者消费者问题
public static void main(String[] args) {
//创建锁和条件
Lock lock = new ReentrantLock();
Condition producerCondition = lock.newCondition();
Condition consumerCondition = lock.newCondition();
Resource resource = new Resource(lock, producerCondition, consumerCondition);
//生产者线程
Producer producer1 = new Producer(resource);
Producer producer2 = new Producer(resource);
Producer producer3 = new Producer(resource);
//消费者线程
Consumer consumer1 = new Consumer(resource);
Consumer consumer2 = new Consumer(resource);
producer1.start();
producer2.start();
producer3.start();
consumer1.start();
// consumer2.start();
}
static class Producer extends Thread {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
sleep((long) (Math.random() * 1000 + 300)); //生产者休息0.5-1.3s
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
static class Consumer extends Thread {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
sleep((long) (Math.random() * 1000)); //消费者休息0-1s
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
static class Resource {
private Lock lock;
private Condition producer;
private Condition customer;
private volatile int num = 0; //库存
private int size = 10; //仓库大小
public Resource(Lock lock, Condition producer, Condition customer) {
this.lock = lock;
this.producer = producer;
this.customer = customer;
}
public void add() {
lock.lock();
try {
if (num < size && num >= 0) {
num++;
customer.signalAll(); //唤醒消费者
System.out.println("生产者"+Thread.currentThread().getName() + "正在生产一件库存,当前库存:" + num);
} else {
System.out.println(Thread.currentThread().getName() + "即将进入等待状态...");
try {
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock(); //解锁
}
}
public void remove() {
lock.lock();
try {
if (num <= size && num > 0) {
num--;
producer.signalAll(); //唤醒生产者
System.out.println("消费者"+Thread.currentThread().getName() + "正在取出一件库存,当前库存:" + num);
} else {
System.out.println(Thread.currentThread().getName() + "即将进入等待状态...");
try {
customer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock(); //解锁
}
}
}
执行结果如下👇:
生产者Thread-2正在生产一件库存,当前库存:1
生产者Thread-0正在生产一件库存,当前库存:2
消费者Thread-3正在取出一件库存,当前库存:1
消费者Thread-3正在取出一件库存,当前库存:0
生产者Thread-0正在生产一件库存,当前库存:1
生产者Thread-1正在生产一件库存,当前库存:2
消费者Thread-3正在取出一件库存,当前库存:1
生产者Thread-1正在生产一件库存,当前库存:2
消费者Thread-3正在取出一件库存,当前库存:1
生产者Thread-0正在生产一件库存,当前库存:2
生产者Thread-2正在生产一件库存,当前库存:3
生产者Thread-1正在生产一件库存,当前库存:4
生产者Thread-0正在生产一件库存,当前库存:5
生产者Thread-2正在生产一件库存,当前库存:6
消费者Thread-3正在取出一件库存,当前库存:5
生产者Thread-1正在生产一件库存,当前库存:6
生产者Thread-1正在生产一件库存,当前库存:7
生产者Thread-0正在生产一件库存,当前库存:8
消费者Thread-3正在取出一件库存,当前库存:7
生产者Thread-2正在生产一件库存,当前库存:8
生产者Thread-0正在生产一件库存,当前库存:9
生产者Thread-1正在生产一件库存,当前库存:10
消费者Thread-3正在取出一件库存,当前库存:9
生产者Thread-1正在生产一件库存,当前库存:10
Thread-2即将进入等待状态…
Thread-0即将进入等待状态…
消费者Thread-3正在取出一件库存,当前库存:9
消费者Thread-3正在取出一件库存,当前库存:8
消费者Thread-3正在取出一件库存,当前库存:7
消费者Thread-3正在取出一件库存,当前库存:6
生产者Thread-1正在生产一件库存,当前库存:7
生产者Thread-0正在生产一件库存,当前库存:8
生产者Thread-1正在生产一件库存,当前库存:9
消费者Thread-3正在取出一件库存,当前库存:8
生产者Thread-2正在生产一件库存,当前库存:9
消费者Thread-3正在取出一件库存,当前库存:8
生产者Thread-0正在生产一件库存,当前库存:9
消费者Thread-3正在取出一件库存,当前库存:8
消费者Thread-3正在取出一件库存,当前库存:7
生产者Thread-2正在生产一件库存,当前库存:8
消费者Thread-3正在取出一件库存,当前库存:7
生产者Thread-1正在生产一件库存,当前库存:8
生产者Thread-2正在生产一件库存,当前库存:9
生产者Thread-1正在生产一件库存,当前库存:10
消费者Thread-3正在取出一件库存,当前库存:9
生产者Thread-0正在生产一件库存,当前库存:10
消费者Thread-3正在取出一件库存,当前库存:9
3.使用阻塞队列BlockingQueue
声明方法:BlockingQueue resourceQueue = new LinkedBlockingQueue(10);
定义最大容量为10,无数据存入时大小为0。
常用方法:
put(e)
:用来将数据存入队列中,若队列满了则会阻塞
take()
:取走一个数据,若数据为空则会阻塞
public static void main(String[] args) {
Resource resource = new Resource();
Producer producer1 = new Producer(resource);
Producer producer2 = new Producer(resource);
Producer producer3 = new Producer(resource);
Customer customer1 = new Customer(resource);
Customer customer2 = new Customer(resource);
Customer customer3 = new Customer(resource);
producer1.start();
producer2.start();
customer1.start();
// customer2.start();
// customer3.start();
}
static class Resource {
// 定义阻塞队列 最大容量为10 默认容量为0
private BlockingQueue resourceQueue = new LinkedBlockingQueue(10);
// private BlockingQueue resourceQueue = new ArrayBlockingQueue(10); 也是一种方式
public void add() {
try {
//向队列中添加元素 内容可以随意取, 队列满时会阻塞
resourceQueue.put(1);
System.out.println("生产者" + Thread.currentThread().getName() + "正在生产一个库存,当前库存为:" + resourceQueue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void remove() {
try {
resourceQueue.take(); //取出一个库存
System.out.println("消费者" + Thread.currentThread().getName() + "正在取出库存,当前库存为:" + resourceQueue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Customer extends Thread {
Resource resource;
public Customer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
static class Producer extends Thread {
Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
sleep( 1000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
执行结果如下,可以观察一下库存的变化👇:
生产者Thread-1正在生产一个库存,当前库存为:2
消费者Thread-3正在取出库存,当前库存为:1
生产者Thread-0正在生产一个库存,当前库存为:2
消费者Thread-3正在取出库存,当前库存为:1
生产者Thread-0正在生产一个库存,当前库存为:1
生产者Thread-1正在生产一个库存,当前库存为:2
消费者Thread-3正在取出库存,当前库存为:1
生产者Thread-1正在生产一个库存,当前库存为:3
生产者Thread-0正在生产一个库存,当前库存为:2
生产者Thread-0正在生产一个库存,当前库存为:4
消费者Thread-3正在取出库存,当前库存为:4
生产者Thread-1正在生产一个库存,当前库存为:4
生产者Thread-1正在生产一个库存,当前库存为:5
消费者Thread-3正在取出库存,当前库存为:5
生产者Thread-0正在生产一个库存,当前库存为:5
生产者Thread-1正在生产一个库存,当前库存为:5
消费者Thread-3正在取出库存,当前库存为:6
生产者Thread-0正在生产一个库存,当前库存为:6
消费者Thread-3正在取出库存,当前库存为:5
生产者Thread-0正在生产一个库存,当前库存为:7
生产者Thread-1正在生产一个库存,当前库存为:7
生产者Thread-1正在生产一个库存,当前库存为:8
消费者Thread-3正在取出库存,当前库存为:8
生产者Thread-0正在生产一个库存,当前库存为:8
生产者Thread-0正在生产一个库存,当前库存为:9
消费者Thread-3正在取出库存,当前库存为:9
生产者Thread-1正在生产一个库存,当前库存为:9
生产者Thread-0正在生产一个库存,当前库存为:10
消费者Thread-3正在取出库存,当前库存为:10
生产者Thread-1正在生产一个库存,当前库存为:10
消费者Thread-3正在取出库存,当前库存为:9
生产者Thread-1正在生产一个库存,当前库存为:10
消费者Thread-3正在取出库存,当前库存为:9
生产者Thread-1正在生产一个库存,当前库存为:10
有人可能会问,为什么这库存数这么奇怪,生产之后还是生产,库存有些却没变,其实当另一个线程获取size时,另一个线程又制造了新的库存,之后又取就是会多了一件,而消费的一件已经被补回来了,所以size才会显示一样,可以这样理解:+1-1为获取size时的库存变化,不相信阻塞成功的同学可以再来看看这几张图:
运行结果:
生产者Thread-1正在生产一个库存,当前库存为:2
生产者Thread-0正在生产一个库存,当前库存为:2
生产者Thread-0正在生产一个库存,当前库存为:3
生产者Thread-1正在生产一个库存,当前库存为:4
生产者Thread-0正在生产一个库存,当前库存为:5
生产者Thread-1正在生产一个库存,当前库存为:6
生产者Thread-0正在生产一个库存,当前库存为:7
生产者Thread-1正在生产一个库存,当前库存为:8
生产者Thread-0正在生产一个库存,当前库存为:10
生产者Thread-1正在生产一个库存,当前库存为:10
然后程序便停一直停在了这,可以数一数共运行了多少次,发现刚好是10次,虽然它库存的显示数量有些奇怪,但并没影响到满队阻塞,只是有些线程在获取size时,另一个线程已经多制造了一个库存。
♦ 总结
生产者消费模式以我的思考来说就是很现实的问题,就是卖家没货了,买家想买,就必须等待卖家生产;而卖家想卖,库存满了没人买,则要等待买家买;而我们通过使用缓冲区将商品放入缓冲区,这样减少了买家和卖家的耦合关系,即双方都不用直接需要对方,而是通过缓冲区来获取商品,减少了双方等待的时长,使得双方能有序执行各自的任务,这是我的拙见,大家也可以想一想,加油!^^