一文理解java多线程之生产者消费者模型(三种实现)

news2025/5/24 10:32:14

生产者消费者模型

本文目录

  • 生产者消费者模型
    • 基本介绍
    • 实现思路
    • synchronized + wait + notify实现
      • 缓冲区
      • 生产者
      • 消费者
      • 测试代码
      • 思考
    • lock + condition实现
      • 缓冲区
      • 生产者、消费者、测试代码
    • 阻塞队列实现
      • 缓冲区
      • 生产者、消费者、测试代码
      • 思考
    • 总结

基本介绍

什么是生产者消费者模型?

在这里插入图片描述
生产者和消费者之间通过一个缓冲区来进行交互,生产者负责生成数据,然后存入缓冲区;消费者则负责消费数据,从缓冲区获取。生产者和消费者只和缓冲区交互,没有直接联系。

其中的同步互斥关系:

  • 生产者之间互斥,消费者之间也互斥

  • 生产者与消费者之间既又同步也有互斥(缓冲区满时,只能消费完再生产;缓冲区空时,只能生产完再消费)

注意:缓存区要先进先出,所以一般用队列实现

为什么要用生产者-消费者模型?

  • 缓解生产者与消费者的速度差异:只能生产一个、再消费一个这样轮换的话,如果生产者和消费者速度差异很大,就会造成等待时间过长的问题。此时可以用一个缓冲区用来存储生产者生产的数据。
  • 解耦:生产者消费者之间没有直接联系,代码不会相互影响

实现思路

多个线程同时对一个资源类进行操作,会并发执行里面的方法。书写代码的时候最好让资源类的线程类分开书写,降低代码的耦合性,资源类只负责填入属性和方法,而线程类负责调用它里面的方法。

对于生产者消费者问题,需要定义三个类:生产者线程类、消费者线程类、缓冲资源类(核心)

  • 缓冲资源类:负责维护一个缓冲区,并且提供一个生产资源的方法、一个消费资源的方法(互斥)
  • 生产者:负责通过缓冲区资源类提供的方法去生产资源
  • 消费者:负责通过缓冲区资源类提供的方法去消费资源

synchronized + wait + notify实现

缓冲区

package 线程基础.生产者消费者模型.synchronized_wait_notify版本;

import java.util.LinkedList;

public class BufferResources {
    private int maxSize = 10;
    //这里用list作为缓冲区,也可以替换为队列
    private LinkedList list = new LinkedList<Integer>();

    public synchronized void consume() {
        while (list.size() == 0) {
            System.out.println(Thread.currentThread().getName() + " 当前缓冲区为空,等待生产中...");
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消费数据,从First开始消费,模拟队列
        Integer value = (Integer) list.removeFirst();
        System.out.println(Thread.currentThread().getName() + " 消费成功:" + value.toString() + " 当前缓冲区size = " + list.size());
        //唤醒所有处于wait状态的线程(包括生产者和消费者)
        notifyAll();
    }

    public synchronized void product(Integer value){
        while (list.size() == maxSize) {
            System.out.println(Thread.currentThread().getName() + " 当前缓冲区满了,等待消费中...");
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //生产数据
        list.add(value);
        System.out.println(Thread.currentThread().getName() + " 生产成功:" + value.toString() + " 当前缓冲区size = " + list.size());
        //唤醒所有处于wait状态的线程(包括生产者和消费者)
        notifyAll();
    }
}

生产者

package 线程基础.生产者消费者模型.synchronized_wait_notify版本;

import java.util.Random;

public class Producter extends Thread {
    private BufferResources bufferResources;
    Random random = new Random();

    //构造时需要指定缓冲区
    public Producter(BufferResources bufferResources) {
        this.bufferResources = bufferResources;
    }

    @Override
    public void run() {
        //生产
        this.bufferResources.product(random.nextInt());
    }
}

消费者

package 线程基础.生产者消费者模型.synchronized_wait_notify版本;

public class Consumer extends Thread {
    private BufferResources bufferResources;

    //构造时需要指定缓冲区
    public Consumer(BufferResources bufferResources) {
        this.bufferResources = bufferResources;
    }

    @Override
    public void run() {
        this.bufferResources.consume();
    }
}

测试代码

package 线程基础.生产者消费者模型;

public class Test {
    public static void main(String[] args) {
        BufferResources bufferResources = new BufferResources();
        //十个生产者线程
        for (int i = 0; i < 10; i++) {
            new Producter(bufferResources).start();
        }
        //十个消费者线程
        for (int i = 0; i < 10; i++) {
            new Consumer(bufferResources).start();
        }

    }
}

测试结果

生产者消费者交错进行生产消费,不会相互影响,资源类的数据一致性得到保证

在这里插入图片描述

思考

上述判断缓冲区是否为满或者空的语句为什么用while循环判断,而不是用if判断?

为了避免虚拟唤醒问题,也就是notifyAll之后,所有线程被唤醒,有可能出现先执行的线程使得后面的线程的不再满足运行条件的现象,如果直接结束wait会造成逻辑错误。具体可参考百度。

上述的唤醒为什么要用notifyAll?而不用notify?

因为notify只会唤醒一个线程去竞争锁,当生产者刚生产完使得缓冲区满的时候,如果又唤醒了一个生产者线程,那程序就会死锁,也叫假死问题

用notifyAll真的合理吗?

其实不合理,因为按照逻辑来说,每生产完一个资源后,应该唤醒消费者去消费,而不需要把生产者也唤醒(消费资源也一样,不需要再唤醒消费者)。这个问题可通过如下的condition实现方式来解决。

lock + condition实现

缓冲区

package 线程基础.生产者消费者模型.lock_condition版本;

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BufferResources {
    private int maxSize = 10;
    //这里用list作为缓冲区,也可以替换为队列
    private LinkedList list = new LinkedList<Integer>();
    //创建锁
    private Lock lock = new ReentrantLock();
    //生产者对应Condition
    private Condition producerCondition = lock.newCondition();
    //消费者对应Condition
    private Condition consumerCondition = lock.newCondition();


    public void consume() {
        lock.lock();
        try{
            while (list.size() == 0) {
                System.out.println(Thread.currentThread().getName() + " 当前缓冲区为空,等待生产中...");
                try {
                    //消费者进入等待状态
                    consumerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //进行消费数据,从First开始消费,模拟队列
            Integer value = (Integer) list.removeFirst();
            System.out.println(Thread.currentThread().getName() + " 消费成功:" + value.toString() + " 当前缓冲区size = " + list.size());
            //消费完毕后,只唤醒生产者
            producerCondition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //一定要用finally执行解锁
            lock.unlock();
        }
    }

    public void product(Integer value){
        lock.lock();
        try{
            while (list.size() == maxSize) {
                System.out.println(Thread.currentThread().getName() + " 当前缓冲区满了,等待消费中...");
                //生产者进入等待状态
                producerCondition.await();
            }
            //进生产数据
            list.add(value);
            System.out.println(Thread.currentThread().getName() + " 生产成功:" + value.toString() + " 当前缓冲区size = " + list.size());
            //生产完毕后,只唤醒消费者
            consumerCondition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //一定要用finally执行解锁
            lock.unlock();
        }
    }
}

生产者、消费者、测试代码

与之前的版本完全一致

测试结果

生产者消费者交错进行生产消费,不会相互影响,资源类的数据一致性得到保证

在这里插入图片描述

分析:用lock的condition实现的好处就是能实现精准地唤醒一类线程

阻塞队列实现

上述的缓冲区都是通过LinkedList简单模拟队列来实现的,但实际上比较常用的是阻塞队列(代码简单),因为它内部已经实现了数据操作的阻塞和互斥功能,具体如下:

  • 如果队列已满,再进行插入则会阻塞,直到队列不满才会被唤醒,然后才能插入
  • 如果队列为空,再进行删除也会进入阻塞,直到队列不为空才被唤醒,然后才能删除

也就是说,生产和消费的代码只需要用阻塞队列进行插入和删除,不需要写额外的互斥和阻塞逻辑了

缓冲区

package 线程基础.生产者消费者模型.阻塞队列版本;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BufferResources {
    private int maxSize = 10;
    //阻塞队列作为缓冲区
    private BlockingQueue buffer = new LinkedBlockingQueue(maxSize);

    public void consume() {
        try {
            Integer value = (Integer) buffer.take();
            System.out.println(Thread.currentThread().getName() + " 消费成功:" + value.toString() + " 当前缓冲区size = " + buffer.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void product(Integer value) {
        try {
            buffer.put(value);
            System.out.println(Thread.currentThread().getName() + " 生产成功:" + value.toString() + " 当前缓冲区size = " + buffer.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

生产者、消费者、测试代码

与之前的版本完全一致

测试结果

在这里插入图片描述

思考

阻塞队列怎么实现阻塞和互斥逻辑的?

其实和我们之前自己实现的思路一样,参考如下LinkedBlockingQueue的put插入源码

在这里插入图片描述

总结

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

  • 用synchronized、wait、notify实现:不能实现精准唤醒,可能出现假死现象(用notify而不是notifyall时)
  • 用lock、condition实现:多个condition配合可以实现精准的唤醒
  • 用阻塞队列实现:代码简单,但需要理解其中阻塞和互斥的原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1593492.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mybatis实体中时间类型LocalDateTime,查询的时候报错

问题描述 Spring boot集成mybatis实体中时间类型LocalDateTime&#xff0c;查询的时候报错 Error attempting to get column create_time from result set. Cause: java.sql.SQLFeatureNotSupportedException原因分析&#xff1a; 因为mybatis和druid的依赖版本兼容问题导致…

DHCP是什么意思 路由器中DHCP服务器怎么设置?

概述 DHCP是什么意思&#xff1f;很多朋友在路由器设置中&#xff0c;都会看到有一项“DHCP服务器”设置功能&#xff0c;而很多朋友对这个功能不太了解&#xff0c;也不知道怎么设置。其实&#xff0c;对于普通用户来说&#xff0c;无需去单独设置路由器DHCP服务器功能&#…

transformer在生物基因DNA的应用:DNABERT、DNABERT-2

参考&#xff1a; https://www.youtube.com/watch?vmk-Se29QPBA&t1388s 写明这些训练模型可以最终训练好可以进行DNA特征向量的提取&#xff0c;应用与后续1、DNABERT https://github.com/jerryji1993/DNABERT 主要思路就是把DNA序列当成连续文本数据&#xff0c;直接用…

【鸿蒙开发】第二十一章 Media媒体服务(一)

1 简介 Media Kit&#xff08;媒体服务&#xff09;提供了AVPlayer和AVRecorder用于播放、录制音视频。 在Media Kit的开发指导中&#xff0c;将介绍各种涉及音频、视频播放或录制功能场景的开发方式&#xff0c;指导开发者如何使用系统提供的音视频API实现对应功能。比如使用…

自己动手封装axios通用方法并上传至私有npm仓库:详细步骤与实现指南

文章目录 一、构建方法1、api/request.js2、api/requestHandler.js3、api/index.js 二、测试方法1、api/axios.js2、main.js3、app.vue4、vue.config.js5、index.html 三、打包1、配置package.json2、生成库包3、配置发布信息4、发布 四、使用1、安装2、使用 五、维护1、维护和…

基于STC12C5A60S2系列1T 8051单片机的带字库液晶显示器LCD12864数据传输并行模式显示图像应用

基于STC12C5A60S2系列1T 8051单片机的液晶显示器LCD12864显示图像应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍液晶显示器LCD12864简单介绍一、LCD12864点阵型液…

react17+18 中 setState是同步还是异步更新

在类组件中使用setState&#xff0c;在函数式组件中使用hooks的useState。 setstate目录 1. 类组件1.1 react 17版本1.2 react 18版本 2、函数式组件 1. 类组件 1.1 react 17版本 参考内容&#xff1a;第十一篇&#xff1a;setState 到底是同步的&#xff0c;还是异步的&…

使用UDP实现TCP的功能,会带来什么好处?

比较孤陋寡闻&#xff0c;只知道QUIC TCPQUIC握手延迟TCP需要三次握手TLS握手三次握手TLS握手放在一起&#xff0c;实现0RTT头阻塞问题TCP丢失保文&#xff0c;会影响所有的应用数据包基于UDP封装传输层Stream&#xff0c;Stream内部保序&#xff0c;Stream之间不存在相互影响…

halcon-轴断面检测定位

前言 通常情况下轴检测时&#xff0c;通常会检测轴的各个阶段的长度。但是由于各种原因&#xff0c;在轴断面的区域现实不明显&#xff0c;无法正确提取&#xff0c;这时候需要根据轴断面的突出部分进行检测&#xff0c;但是由于部分轴的粗轴和细轴区域的宽度差距相当接近&…

Linux部署自动化运维平台Spug

文章目录 前言1. Docker安装Spug2 . 本地访问测试3. Linux 安装cpolar4. 配置Spug公网访问地址5. 公网远程访问Spug管理界面6. 固定Spug公网地址 前言 Spug 面向中小型企业设计的轻量级无 Agent 的自动化运维平台&#xff0c;整合了主机管理、主机批量执行、主机在线终端、文件…

C/C++基础----常量和基本数据类型

HelloWorld #include <iostream>using namespace std;int main() {// 打印cout << "Hello,World!" << endl;return 0; }c/c文件和关系 c和c是包含关系&#xff0c;c相当于是c的plus版本c的编译器也可以编译c语言c文件.cpp结尾.h为头文件.c为c语言…

C++中的STL——list类的基本使用

目录 list类介绍 list类定义 list类常见构造 list类的有效元素个数操作 size()函数 list遍历操作 list元素修改操作 assign()函数 push_front()函数 push_back()函数 pop_front()函数 pop_back()函数 insert()函数 erase()函数 swap()函数 resize()函数 clear…

Mac环境 llamafile 部署大语言模型LLM

文章目录 Github官网本地部署 llamafile 是一种可在你自己的电脑上运行的可执行大型语言模型&#xff08;LLM&#xff09;&#xff0c;它包含了给定的开放 LLM 的权重&#xff0c;以及运行该模型所需的一切。让人惊喜的是&#xff0c;你无需进行任何安装或配置。 Github https…

CSS3新增

一些CSS3新增的功能 课程视频链接 目录 CSS3概述私有前缀长度单位remvwvhvmaxvmin 颜色设置方式rgbahslhsla 选择器动态伪类目标伪类语言伪类UI伪类结构伪类否定伪类伪元素 盒子属性box-sizing问题插播 宽度与设置的不同 resizebox-shadowopacity 背景属性background-originb…

CCS在线调试时实时修改变量值

在使用CCS调试dsp芯片时&#xff0c;发现CCS软件有一个非常好的功能&#xff0c;在仿真调试的时候可以实时修改代码中变量的值。这个功能在调试switch语句的时候非常好用&#xff0c;比如想要执行哪个case语句&#xff0c;直接在仿真界面里面修改switch语句入口参数就行。   …

机器学习周记(第三十四周:文献阅读[GNet-LS])2024.4.8~2024.4.14

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文模型 1.3.1 数据处理 1.3.2 GNet-LS 2 相关代码 摘要 本周阅读了一篇时间序列预测论文。论文模型为GNet-LS&#xff0c;主要包含四个模块&#xff1a;粒度划分模块&#xff08;GD&#xff09;&…

回归预测 | Matlab实现WOA-BP鲸鱼算法优化BP神经网络多变量回归预测

回归预测 | Matlab实现WOA-BP鲸鱼算法优化BP神经网络多变量回归预测 目录 回归预测 | Matlab实现WOA-BP鲸鱼算法优化BP神经网络多变量回归预测预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab实现WOA-BP鲸鱼算法优化BP神经网络多变量回归预测&#xff08;完整源码…

通过一篇文章让你了解Linux的重要性

Linux 前言一、什么是Linux后台vs前台为何大多数公司选择使用Linux作为后台服务器 二、Linux的背景介绍UNIX发展的历史Linux发展历史开源官网发行版本DebianUbuntu红帽企业级LinuxCentOSFedoraKali Linux 三、国内企业后台和用户使用Linux现状IT服务器Linux系统应用领域嵌入式L…

容器受到攻击时该如何应对,容器安全给你答案

容器是一个软件包&#xff0c;其中包含在任何操作系统和基础架构上运行所需的所有依赖项&#xff0c;包括代码、配置文件、库和系统工具。每个容器都包含一个运行时环境&#xff0c;使应用程序能够在各种计算环境之间迁移——例如&#xff0c;从物理机迁移到云。 容器提供了许…

【C++类和对象】构造函数与析构函数

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…