Java多线程——生产者消费者问题

article/2025/11/10 2:51:02

创建多个线程去执行不同的任务,如果这些任务之间有着某种关系,那么线程之间必须能够通信来协调完成工作。
在这里插入图片描述
生产者消费者问题(英语:Producer-consumer problem)就是典型的多线程同步案例,它也被称为有限缓冲问题(英语:Bounded-buffer problem)。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

注意:生产者-消费者模式中的内存缓存区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者的性能差;

准备基础代码:无通信的生产者消费者

我们来自己编写一个例子:一个生产者,一个消费者,并且让他们让他们使用同一个共享资源,并且我们期望的是生产者生产一条放到共享资源中,消费者就会对应地消费一条。

我们先来模拟一个简单的共享资源对象:

publicclass ShareResource {private String name;private String gender;/*** 模拟生产者向共享资源对象中存储数据** @param name* @param gender*/public void push(String name, String gender) {this.name = name;this.gender = gender;}/*** 模拟消费者从共享资源中取出数据*/public void popup() {System.out.println(this.name + "-" + this.gender);}
}

然后来编写我们的生产者,使用循环来交替地向共享资源中添加不同的数据:

publicclass Producer implements Runnable {private ShareResource shareResource;public Producer(ShareResource shareResource) {this.shareResource = shareResource;}@Overridepublic void run() {for (int i = 0; i < 50; i++) {if (i % 2 == 0) {shareResource.push("凤姐", "女");} else {shareResource.push("张三", "男");}}}
}

接着让我们的消费者不停地消费生产者产生的数据:

publicclass Consumer implements Runnable {private ShareResource shareResource;public Consumer(ShareResource shareResource) {this.shareResource = shareResource;}@Overridepublic void run() {for (int i = 0; i < 50; i++) {shareResource.popup();}}
}

然后我们写一段测试代码,来看看效果:

public static void main(String[] args) {// 创建生产者和消费者的共享资源对象ShareResource shareResource = new ShareResource();// 启动生产者线程new Thread(new Producer(shareResource)).start();// 启动消费者线程new Thread(new Consumer(shareResource)).start();
}

我们运行发现出现了诡异的现象,所有的生产者都似乎消费到了同一条数据:

张三-男
张三-男
....以下全是张三-男....

为什么会出现这样的情况呢?照理说,我的生产者在交替地向共享资源中生产数据,消费者也应该交替消费才对呀…我们大胆猜测一下,会不会是因为消费者是直接循环了 30 次打印共享资源中的数据,而此时生产者还没有来得及更新共享资源中的数据,消费者就已经连续打印了 30 次了,所以我们让消费者消费的时候以及生产者生产的时候都小睡个 10 ms 来缓解消费太快 or 生产太快带来的影响,也让现象更明显一些:

/*** 模拟生产者向共享资源对象中存储数据** @param name* @param gender*/
public void push(String name, String gender) {try {Thread.sleep(10);} catch (InterruptedException ignored) {}this.name = name;this.gender = gender;
}/*** 模拟消费者从共享资源中取出数据*/
public void popup() {try {Thread.sleep(10);} catch (InterruptedException ignored) {}System.out.println(this.name + "-" + this.gender);
}

再次运行代码,发现了出现了以下的几种情况:

重复消费:消费者连续地出现两次相同的消费情况(张三-男/ 张三-男);
性别紊乱:消费者消费到了脏数据(张三-女/ 凤姐-男);

分析出现问题的原因

重复消费:我们先来看看重复消费的问题,当生产者生产出一条数据的时候,消费者正确地消费了一条,但是当消费者再来共享资源中消费的时候,生产者还没有准备好新的一条数据,所以消费者就又消费到老数据了,这其中的根本原因是生产者和消费者的速率不一致
性别紊乱:再来分析第二种情况。不同于上面的情况,消费者在消费第二条数据时,生产者也正在生产新的数据,但是尴尬的是,生产者只生产了一半儿(也就是该执行完 this.name = name),也就是还没有来得及给 gender 赋值就被消费者给取走消费了… 造成这样情况的根本原因是没有保证生产者生产数据的原子性

解决出现的问题

加锁解决性别紊乱

我们先来解决性别紊乱,也就是原子性的问题吧,上一篇文章里我们也提到了,对于这样的原子性操作,解决方法也很简单:加锁。稍微改造一下就好了:

/*** 模拟生产者向共享资源对象中存储数据** @param name* @param gender*/
synchronized public void push(String name, String gender) {this.name = name;try {Thread.sleep(10);} catch (InterruptedException ignored) {}this.gender = gender;
}/*** 模拟消费者从共享资源中取出数据*/
synchronized public void popup() {try {Thread.sleep(10);} catch (InterruptedException ignored) {}System.out.println(this.name + "-" + this.gender);
}

我们在方法前面都加上了 synchronized 关键字,来保证每一次读取和修改都只能是一个线程,这是因为当 synchronized 修饰在普通同步方法上时,它会自动锁住当前实例对象,也就是说这样改造之后读/ 写操作同时只能进行其一;
我把 push 方法小睡的代码改在了赋值 name 和 gender 的中间,以强化验证原子性操作是否成功,因为如果不是原子性的话,就很可能出现赋值 name 还没赋值给 gender 就被取走的情况,小睡一会儿是为了加强这种情况的出现概率(可以试着把 synchronized 去掉看看效果);
运行代码后发现,并没有出现性别紊乱的现象了,但是重复消费仍然存在。

等待唤醒机制解决重复消费

我们期望的是 张三-男 和 凤姐-女 交替出现,而不是有重复消费的情况,所以我们的生产者和消费者之间需要一点沟通,最容易想到的解决方法是,我们新增加一个标志位,然后在消费者中使用 while 循环判断,不满足条件则不消费,条件满足则退出 while 循环,从而完成消费者的工作。

while (value != desire) {Thread.sleep(10);
}
doSomething();

这样做的目的就是为了防止「过快的无效尝试」,这种方法看似能够实现所需的功能,但是却存在如下的问题:

1)难以确保及时性。在睡眠时,基本不消耗处理器的资源,但是如果睡得过久,就不能及时发现条件已经变化,也就是及时性难以保证;
2)难以降低开销。如果降低睡眠的时间,比如休眠 1 毫秒,这样消费者能够更加迅速地发现条件变化,但是却可能消耗更多的处理资源,造成了无端的浪费。
以上两个问题吗,看似矛盾难以调和,但是 Java 通过内置的等待/ 通知机制能够很好地解决这个矛盾并实现所需的功能。

等待/ 通知机制,是指一个线程 A 调用了对象 O 的 wait() 方法进入等待状态,而另一个线程 B 调用了对象 O 的 notifyAll() 方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。上述两个线程都是通过对象 O 来完成交互的,而对象上的 wait 和 notify/ notifyAll 的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

这里有一个比较奇怪的点是,为什么看起来像是线程之间操作的 wait 和 notify/ notifyAll 方法会是 Object 类中的方法,而不是 Thread 类中的方法呢?

简单来说:因为 synchronized 中的这把锁可以是任意对象,因为要满足任意对象都能够调用,所以属于 Object 类;
专业点说:因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的 notify 唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在 Object 类中。

好,简单介绍完等待/ 通知机制,我们开始改造吧:

publicclass ShareResource {private String name;private String gender;// 新增加一个标志位,表示共享资源是否为空,默认为 trueprivateboolean isEmpty = true;/*** 模拟生产者向共享资源对象中存储数据** @param name* @param gender*/synchronized public void push(String name, String gender) {try {while (!isEmpty) {// 当前共享资源不为空的时,则等待消费者来消费// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒this.wait();}// 开始生产this.name = name;Thread.sleep(10);this.gender = gender;// 生产结束isEmpty = false;// 生产结束唤醒一个消费者来消费this.notify();} catch (Exception ignored) {}}/*** 模拟消费者从共享资源中取出数据*/synchronized public void popup() {try {while (isEmpty) {// 为空则等着生产者进行生产// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒this.wait();}// 消费开始Thread.sleep(10);System.out.println(this.name + "-" + this.gender);// 消费结束isEmpty = true;// 消费结束唤醒一个生产者去生产this.notify();} catch (InterruptedException ignored) {}}
}

我们期望生产者生产一条,然后就去通知消费者消费一条,那么在生产和消费之前,都需要考虑当前是否需要生产 or 消费,所以我们新增了一个标志位来判断,如果不满足则等待;

被通知后仍然要检查条件,条件满足,则执行我们相应的生产 or 消费的逻辑,然后改变条件(这里是 isEmpty),并且通知所有等待在对象上的线程;

注意:上面的代码中通知使用的 notify() 方法,这是因为例子中写死了只有一个消费者和生产者,在实际情况中建议还是使用 notifyAll() 方法,这样多个消费和生产者逻辑也能够保证(可以自己试一下);

小结

通过初始版本一步步地分析问题和解决问题,我们就差不多写出了我们经典生产者消费者的经典代码,但通常消费和生产的逻辑是写在各自的消费者和生产者代码里的,这里我为了方便阅读,把他们都抽离到了共享资源上,我们可以简单地再来回顾一下这个消费生产和等待通知的整个过程:
在这里插入图片描述
以上就是关于生产者生产一条数据,消费者消费一次的过程了,涉及的一些具体细节我们下面来说。


http://chatgpt.dhexx.cn/article/6ihNMjj3.shtml

相关文章

生产者-消费者问题(详解)

目录 1.问题描述 2.问题分析 3.问题实现 3.1 初始化 3.2 生产者 3.3 消费者 1.问题描述 要求如下&#xff1a; 只要缓冲区没满&#xff0c;生产者才能把产品放入缓冲区&#xff0c;否则必须等待。只有缓冲区不空时&#xff0c;消费者才能从中取出产品&#xff0c;否则必…

【操作系统】生产者消费者问题

生产者消费者模型 文章目录 生产者消费者模型 [toc]一、 生产者消费者问题二、 问题分析三、 伪代码实现四、代码实现&#xff08;C&#xff09;五、 互斥锁与条件变量的使用比较 一、 生产者消费者问题 生产者消费者问题&#xff08;英语&#xff1a;Producer-consumer proble…

Sublime Text实现代码自动生成,快速编写HTML/CSS代码

目录 下载Sublime Text安装emmet插件常用自动生成HTML代码实例初始化页面自动补全标签配对自动添加类名和id名自动填充文本内容自动生成同级标签自动生成嵌套标签自动生成提级标签自动生成分组标签自动生成多个元素自动生成带多个属性的元素自动生成隐式标签 常用自动生成CSS代…

MybatisPlus代码自动生成

这里写自定义目录标题 前言一. 什么是 MyBatis-Plus二.MybatisPlus 代码自动生成①idea 插件生成1. 插件2.连接数据源3.生成代码 ②配置工具类生成 前言 最开始&#xff0c;要在 Java 中使用数据库时&#xff0c;需要使用 JDBC&#xff0c;创建 Connection、ResultSet 等&…

Simulink自动代码生成:生成代码的基本设置

Simulink自动代码生成也被称作基于模型开发&#xff08;BMD&#xff09;&#xff0c;相比于传统的手写代码方式能够尽量减少人为错误。模型本身可以用于仿真&#xff0c;单元测试等&#xff0c;更便于提前发现逻辑错误。同时只要约定好模型接口&#xff0c;就可以多人协作&…

C语言代码自动生成工具

一、模型建模模块&#xff1a; 基于开源开发平台Eclipse&#xff0c;以图形方式创建和编辑模型元素&#xff0c;模型元素如下&#xff1a; 活动&#xff1a;初始活动、简单活动、复杂活动、结束活动&#xff1b;状态&#xff1a;初始状态、状态、结束状态&#xff1b;变迁&a…

前端代码自动生成器

场景 1.CodeFun是什么 CodeFun是一款UI 设计稿智能生成源代码的工具,支持微信小程序端、移动端H5和混合APP,上传 Sketch、PSD等形式的设计稿&#xff0c;通过智能化技术一键生成可维护的前端代码. 2.学习成本高吗&#xff1f; 对于前端工程师来说&#xff0c;几乎没有学习成本…

MATLAB/Simulink自动代码生成(一)

Simulink自带了种类繁多、功能强大的模块库&#xff0c;在基于模型设计的开发流程下&#xff0c;Simulink不仅通过仿真可以进行早期设计的验证&#xff0c;还可以生成C/C、PLC等代码直接应用于PC、MCU、DSP等平台。在嵌入式软件开发中发挥着重要的作用&#xff0c;本文以Simuli…

IDEA自动生成代码插件

官方介绍 基于IntelliJ IDEA开发的代码生成插件&#xff0c;支持自定义任意模板&#xff08;Java&#xff0c;html&#xff0c;js&#xff0c;xml&#xff09;。 只要是与数据库相关的代码都可以通过自定义模板来生成。支持数据库类型与java类型映射关系配置。 支持同时生成生…

Matlab/Simulink自动生成C代码实验

目录 0. 概要 1. Matlab /Simulink/Embedded Coder关系与区别 2. 搭建Simulink模型及仿真 2.1 搭建模型 2.2 仿真 3. 生成代码 3.1 求解器设置为定步长 3.2 安装 MinGW-w64 编译器 3.3 调出Simulink Coder 4. 工具都生成了啥呢&#xff1f; 0. 概要 Matlab网站提供了很多…

关于RuoYi自动代码生成功能的使用

为什么要使用代码生成&#xff1f; 答&#xff1a;因为在后端构建的过程中会有许多重复的类似的代码编写&#xff0c;而我们如果一个个去编写&#xff0c;会耗费大量时间与精力&#xff0c;所以我们可以设计一个功能去自动生成这些重复的&#xff0c;简单的代码。而若依系统就…

Mybatis Plus自动生成代码

mybatis-plus自动生成代码 一、简易生成代码二、指定生成的样式&#xff0c;并且不在一个模块1.父pom文件配置2.子模块pom文件配置3.准备vm文件4.设置MyBatisPlusGenerator.java5.运行MyBatisPlusGenerator.java文件6.运行sign-auth模块,解决异常 一、简易生成代码 /*** 代码生…

Simulink自动代码生成:数据类型别名自定义

在手写代码时&#xff0c;我们经常能看到自定义数据类型别名&#xff0c;例如有些代码中将计算机默认的数据类型改为我们自己习惯的名称&#xff0c;如图所示。 目录 一. 系统默认生成的别名二. 建立Simulink AliasType三. 修改Data Type Replacement四. 数据类型别名修改后的…

Simulink 自动代码生成原理

如下图&#xff0c;Simulink模型会先变成一个文本式的 .rtw 模型描述文件&#xff0c;然后再变成 .c,.h&#xff0c;最后编译为最终目标文件。 典型的 Simulink 用户通常都是&#xff0c;用Simulink设计好算法后&#xff0c;做到生成源代码这一步。然后把生成的算法的.c .h 源代…

如何自动生成SpringBoot项目代码

目录 1.RuoYi源码下载及启动若依服务1.1. RuoYi源码下载1.2. 启动若依服务 2.自动生成代码3.代码及sql文件链接 已经工作一段时间啦&#xff01;首先是从后端开发开始入手的&#xff0c;前端也是在自学阶段&#xff08;边学边问我身边的同事大佬&#xff09;&#xff0c;努力是…

Simulink自动代码生成:数据字典的建立及代码优化

在上一节《Simulink自动代码生成&#xff1a;生成代码的基本设置》的基础上&#xff0c;我们来对模型进行优化&#xff0c;使得生成的代码更能满足实际的需求&#xff0c;没看过我上一篇文章的可以点开如下链接&#xff1a;   Simulink自动代码生成&#xff1a;生成代码的基本…

推荐几个代码自动生成器

文章目录 老的代码生成器的地址&#xff1a;[https://www.cnblogs.com/skyme/archive/2011/12/22/2297592.html](https://link.zhihu.com/?targethttps%3A//www.cnblogs.com/skyme/archive/2011/12/22/2297592.html)1.懒猴子CG2.IT猿网3.listcode4.magicalcoder5.CodeSmith6. …

Mybatis代码自动生成

新启动的项目,数据库设计可能随时会变动,一些基础的接口,特别是xml文件和映射对象也需要变动,改动工作量大,用mybatis-plus代码自动生成工具自动生成代码,大大提高了效率 自动生成代码工具使用过程记录如下 首先手动创建一个springboot项目,可以去springboot官网上生成,也可以…

Simulink 自动代码生成电机控制:基于Keil软件集成

目录 系统软件架构 1.应用层全模型生成&#xff0c;底层手写代码 2.应用层模型生成&#xff0c;底层也是基于模型生成 3.Autosar 软件集成操作 接口配置 总结 系统软件架构 嵌入式软件开发包含应用层和底层&#xff0c;目前基于模型的开发软件架构总结为以下几种: 1.应…

mybatis自动生成代码

mybatis自动生成代码有三种方式&#xff1a;命令行、eclipse插件、maven插件。在这里主要介绍比较方便使用的一种方式–maven插件&#xff0c;它可以在eclipse、idea中通用。 在pom.xml文件中配置mybatis-generator插件&#xff1a; <plugin><groupId>org.mybatis…