一、前言

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

生产者消费者模型需要抓住“三个主体,三个要点“,三个主体是指:生产者消费者缓冲区。生产者往缓冲区放数据,消费者从缓冲区取数据。

三个要点是指:

1.缓冲区有固定大小
2.缓冲区满时,生产者不能再往缓冲区放数据(产品),而是被阻塞,直到缓冲区不是满的
3.缓冲区为空时,消费者不能再从缓冲区取数据,而是被阻塞,直到缓冲区不是空的。

数据(产品)往往是先生产出来的先被消费。所以缓冲区一般用有界队列实现,又由于生产者、消费者在特定情况下需要被阻塞,所以更具体一点,缓冲区一般用有界阻塞队列来实现。
本篇用三种方式实现生产者-消费者模型:wait/notify + 队列、Lock/Condition + 队列、有界阻塞队列。

二、wait/notify + 队列

实现生产者-消费者模型,主要是实现两个核心方法:往缓冲区中放元素、从缓冲区中取元素。
以下是缓冲区的代码实现,是生产者-消费者模型的核心。

ProducerConsumerQueue缓冲区
package cn.java.threadmodel.producerconsumer;import java.util.LinkedList;
import java.util.Queue;
/*** @author 小石潭记* @date 2021/12/18 9:58* @Description: wait/notify机制实现生产者-消费者模型*/
public class ProducerConsumerQueue<E> {/*** 队列最大容量*/private final static int QUEUE_MAX_SIZE = 3;/*** 存放元素的队列*/private Queue<E> queue;public ProducerConsumerQueue() {queue = new LinkedList<>();}/*** 向队列中添加元素** @param e* @return*/public synchronized boolean put(E e) {// 如果队列是已满,则阻塞当前线程while (queue.size() == QUEUE_MAX_SIZE) {try {wait();} catch (InterruptedException e1) {e1.printStackTrace();}}// 队列未满,放入元素,并且通知消费线程queue.offer(e);System.out.println(Thread.currentThread().getName() + " -> 生产元素,元素个数为:" + queue.size());notify();return true;}/*** 从队列中获取元素* @return*/public synchronized E get() {// 如果队列是空的,则阻塞当前线程while (queue.isEmpty()) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}// 队列非空,取出元素,并通知生产者线程E e = queue.poll();System.out.println(Thread.currentThread().getName() + " -> 消费元素,元素个数为:" + queue.size());notify();return e;}
}
Producer生产者
package cn.java.threadmodel.producerconsumer;/*** @author 小石潭记* @date 2021/12/18 10:00* @Description: 生产者线程*/
public class Producer implements Runnable {private ProducerConsumerQueue<Integer> queue;public Producer(ProducerConsumerQueue<Integer> queue) {this.queue = queue;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {queue.put(i);}}
}
Consumer消费者
package cn.java.threadmodel.producerconsumer;/*** @author 小石潭记* @date 2021/12/18 10:00* @Description: 消费者线程*/
public class Consumer implements Runnable {private ProducerConsumerQueue<Integer> queue;public Consumer(ProducerConsumerQueue<Integer> queue) {this.queue = queue;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {queue.get();}}
}
ProducerConsumerDemo测试类
package cn.java.threadmodel.producerconsumer;import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author 小石潭记* @date 2021/12/18 10:01* @Description: 测试生产消费者模型*/
public class ProducerConsumerDemo {private final static ExecutorService service = Executors.newCachedThreadPool();public static void main(String[] args) throws InterruptedException {Random random = new Random();// 生产者-消费者模型缓冲区ProducerConsumerQueue<Integer> queue = new ProducerConsumerQueue<>();Producer producer = new Producer(queue);Consumer consumer = new Consumer(queue);for (int i = 0; i < 3; i++) {// 休眠0-50毫秒,增加随机性Thread.sleep(random.nextInt(50));service.submit(producer);}for (int i = 0; i < 3; i++) {// 休眠0-50毫秒,增加随机性Thread.sleep(random.nextInt(50));service.submit(consumer);}// 关闭线程池service.shutdown();}
}

从上图的测试结果得知:

  • 由于队列的最大长度是3(QUEUE_MAX_SIZE),所以缓冲区元素不会超过3,说明缓冲区满时,生产者确实被阻塞了
  • 缓冲区元素个数最小为0,不会出现负数,说明缓冲区为空时,消费者被阻塞了

这就是生产者-消费者模型基于wait/notify+队列的基本实现。

三、Lock/Condition + 队列

核心部分缓冲区的实现代码实现如下:

package cn.java.threadmodel.producerconsumer.lockcondition;import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*** @author 小石潭记* @date 2021/12/18 10:11* @Description: Lock/Condition实现生产者-消费者模型*/
public class ProducerConsumerQueue<E> {/*** 队列最大容量*/private final static int QUEUE_MAX_SIZE = 3;/*** 存放元素的队列*/private Queue<E> queue;private final Lock lock = new ReentrantLock();private final Condition producerCondition = lock.newCondition();private final Condition consumerCondition = lock.newCondition();public ProducerConsumerQueue() {queue = new LinkedList<>();}/*** 向队列中添加元素* @param e* @return*/public boolean put(E e) {final Lock lock = this.lock;lock.lock();try {while (queue.size() == QUEUE_MAX_SIZE) {// 队列已满try {producerCondition.await();} catch (InterruptedException e1) {e1.printStackTrace();}}queue.offer(e);System.out.println(Thread.currentThread().getName() + " -> 生产元素,元素个数为:" + queue.size());consumerCondition.signal();} finally {lock.unlock();}return true;}/*** 从队列中取出元素* @return*/public E get() {final Lock lock = this.lock;lock.lock();try {while (queue.isEmpty()) {// 队列为空try {consumerCondition.await();} catch (InterruptedException e1) {e1.printStackTrace();}}E e = queue.poll();System.out.println(Thread.currentThread().getName() + " -> 消费元素,元素个数为:" + queue.size());producerCondition.signal();return e;} finally {lock.unlock();}}
}

生产者线程、消费者线程、测试代码更是和wait/notify方式一致。

四、有界阻塞队列

同样,缓冲区的实现也是其核心部分,不过阻塞队列已经提供了相应的阻塞API,所以不需要额外编写阻塞部分的代码。

package cn.java.threadmodel.producerconsumer.queue;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;/*** @author 小石潭记* @date 2021/12/18 10:23* @Description:  * 阻塞队列实现生产者-消费者模型*                * 对应的阻塞方法是put()/take()*/
public class ProducerConsumerQueue<E> {/*** 队列最大容量*/private final static int QUEUE_MAX_SIZE = 3;/*** 存放元素的队列*/private BlockingQueue<E> queue;public ProducerConsumerQueue() {queue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);}/*** 向队列中添加元素* @param e* @return*/public boolean put(E e) {try {queue.put(e);System.out.println(Thread.currentThread().getName() + " -> 生产元素,元素个数为:" + queue.size());} catch (InterruptedException e1) {e1.printStackTrace();}return true;}/*** 从队列中取出元素* @return*/public E get() {try {E e = queue.take();System.out.println(Thread.currentThread().getName() + " -> 消费元素,元素个数为:" + queue.size());return e;} catch (InterruptedException e1) {e1.printStackTrace();}return null;}
}

生产者线程、消费者线程、测试代码也和前面两种一模一样。

五、总结

通过三种方式实现生产者-消费者模型,可以看出使用阻塞队列的方式最简单,也更安全。其实看看阻塞队列的源码,会发现其内部的实现和这里的前两种差不多,只是JDK提供的阻塞队列健壮性更好。

说完了三种实现方式,再来说说为什么要使用生产者-消费者模式,消费者直接调用生产者不好吗?
回顾文章开始的那张图,试想一下,如果没有生产者-消费者模式会怎样,大概会变成如下这样

可以看到,三个生产者,三个消费者就会产生 3 * 3 = 9条调用关系(箭头方法代表数据走向),还有一点就是消费者也有可能还是生产者,生产者也有可能还是消费者,一旦生产者、消费者的数量多了之后就会形成复杂的调用网。所以生产者-消费者模型的最大好处就是解耦。
其次如果生产者和消费者的速度上有较大的差异,就一定会存在一方总是在等待另一方的情况。比如快递小哥如果每一个快递都必须直接送到用户手上,如果某个用户一直联系不上,或者说过了很久才取快递,那么快递小哥就只能一直等待。所以就出现了快递站,快递小哥只需要把快递放在指定位置,用户去指定位置取就行了。所以生产者-消费者模型的第二个好处就是平衡生产能力和消费能力的差异。

六、参考

三种方式实现生产者-消费者模型

多线程-生产者-消费者模型相关推荐

  1. python多线程实现生产者消费者_用Python实现多线程“生产者-消费者”模型的简单例子...

    用 Python 实现多线程"生产者 - 消费者"模型的简单例子 生产者消费者问题是一个著名的线程同步问题, 该问题描述如下: 有一个生产者在生产产品, 这些产品将提供给若干个消费 ...

  2. 多线程生产者消费者模型

    1. 基础知识: 1. 什么是生产者-消费者模式: 比如有两个进程A和B,它们共享一个固定大小的缓冲区,A进程产生数据放入缓冲区,B进程从缓冲区中取出数据进行计算,那么这里其实就是一个生产者和消费者的 ...

  3. Linux多线程——生产者消费者模型

    目录 一.生产者消费者模型 1.1 什么是生成者消费者模型 1.2 生产者消费者模型的优点 1.3 基于阻塞队列实现生产者消费者模型 1.4 POSIX信号量 1.4.1 信号量概念 1.4.2 P操 ...

  4. Linux基于单链表环形队列的多线程生产者消费者模型

    生产者–消费者模型简述 对于生产者–消费者模型,相信我们都不陌生,因为生活中,我们无时无刻不在扮演生产者或消费者.但是对于Linux中的生产者–消费者模型,大家又了解了一个什么程度? 其实,说白了就是 ...

  5. 多线程——生产者消费者模型

    写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用. 1.synchronized同步方法 public class Con ...

  6. java多线程抽奖_java 线程池、多线程并发实战(生产者消费者模型 1 vs 10) 附案例源码...

    导读 前二天写了一篇<Java 多线程并发编程>点我直达,放国庆,在家闲着没事,继续写剩下的东西,开干! 线程池 为什么要使用线程池 例如web服务器.数据库服务器.文件服务器或邮件服务器 ...

  7. C++11 并发指南九(综合运用: C++11 多线程下生产者消费者模型详解)

    前面八章介绍了 C++11 并发编程的基础(抱歉哈,第五章-第八章还在草稿中),本文将综合运用 C++11 中的新的基础设施(主要是多线程.锁.条件变量)来阐述一个经典问题--生产者消费者模型,并给出 ...

  8. Java多线程(十):BlockingQueue实现生产者消费者模型

    BlockingQueue BlockingQueue.解决了多线程中,如何高效安全"传输"数据的问题.程序员无需关心什么时候阻塞线程,什么时候唤醒线程,该唤醒哪个线程. 方法介绍 ...

  9. 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池

    并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...

最新文章

  1. 搭建Cacti监控系统(一)-- 搭建LNMP 环境
  2. c程序封装linux,Linux系统使用C语言封装线程读写锁
  3. 智能机器人服务广州春运
  4. java五子棋以当前空位为中心 取9个点_java 五子棋有点问题,哪位帮忙破一下、、...
  5. kmalloc、vmalloc、malloc的区别
  6. sql 如何根据月份查询数据总数_什么是慢查询?如何通过慢查询日志优化?
  7. 【LeetCode笔记】41. 缺失的第一个正数(Java、哈希)
  8. portainer MySQL_portainer
  9. JS在与lua的交互心得
  10. [渝粤教育] 四川大学 西方经济学(微观) 参考 资料
  11. javascript事件委托和jQuery事件绑定on、off 和one以及on绑定多个事件(重要)
  12. 编码的奥秘:编码与组合
  13. wincc 服务器授权型号,WinCC V7.5 SP1软件安装及授权方法
  14. 计算机软考初级信息技术试题及答案,2015年软考信息技术处理员考试模拟试题及答案...
  15. 计算机怎么硬盘重做系统,怎么为双硬盘的电脑重装系统
  16. java中数组集合存放位置_java中数组以及集合
  17. xp计算机找不到音量调节,winxp系统电脑音量无法调节不能调节声音的恢复方案...
  18. 基于单片机的无线防盗报警系统设计(#0449)
  19. 渐变背景和背景图片并存
  20. 多旋翼无人机ROSC++开发例程(四):基于Prometheus开源项目与Casadi开源优化求解器的模型预测控制简单应用例程

热门文章

  1. 虚拟机初始化脚本, 虚拟机相互免秘钥
  2. @PathVariable注解
  3. 重庆交通大学c语言上机试题,2021考研复试重庆交通大学《C语言程序设计》复试大纲...
  4. X86保护模式下的内存寻址
  5. wps2007版本如何生成目录的功能预览
  6. linux 计价软件,开源实时计费系统 asterCC
  7. PTA 7-10 公路村村通 (30分)
  8. css3马赛克西瓜js特效代码
  9. App性能优化(一)—— 启动优化,冷启动,热启动,温启动
  10. Dexguard分析钛备份破解