【Java多线程案例】使用阻塞队列实现生产者消费者模型

news2024/9/29 3:24:39

前言

本篇文章讲解多线程案例之阻塞队列。主要讲解阻塞队列的特性、实际开发中常用的到的生产者消费者模型,以及生产者消费者模型解耦合、削峰填谷的好处。并且使用 Java 多线程模拟实现一个生产者消费者模型、阻塞队列版的生产者消费者模型。

文章从什么是阻塞队列、生产者消费者模型、高内聚低耦合、削峰填谷、模拟实现生产者消费者模型、阻塞队列版消费者模型,这几个模块来讲解。话不多说,让我们进入 阻塞队列 的学习吧~

目录

1. 什么是阻塞队列

2. 生产者消费者模型

2.1. 解耦合

2.2 削峰填谷

2.3 生产者消费者案例

3. 阻塞队列生产者消费者模型的实现


1. 什么是阻塞队列

在数据结构的学习中,我们知道了队列有普通队列、循环队列,它们都遵循“先进先出”的原则。阻塞队列也遵循这个原则,它是一种特殊的队列(带有阻塞功能的队列),并且满足以下两点:

  • 当队列的时候,如果继续往队列中插入数据,则发生阻塞状态,直到有数据出队列。
  • 当队列的时候,如果往外取数据,也发生阻塞状态,直到有数序入队列。

Java 标准库中的阻塞队列为:BlockingDeque<>,是一个泛型接口。因此,我们使用的时候直接遵循标准库的写法即可。注意以下两点:

  1. BlockingDeque 是一个接口,因此我们实例对象时用的是 LinkedBlockingQueue类。
  2. put 方法用于阻塞式的入队列, take 用于阻塞式的出队列。

通过上述介绍,我们可以写出一段简易的阻塞队列代码:

    public static void main(String[] args) throws InterruptedException {
        //BlockingQueue<>为阻塞队列的原型
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
        //take(取元素)、put(插入元素)为阻塞队列的两个核心方法
        blockingQueue.put(20);//插入元素20
        Integer result = blockingQueue.take();//从队头取元素
        System.out.println(result);
    }

运行后打印:

通过上述代码,大家已经对阻塞队列有了一个浅的认识,当然你可以可以多 take 几次来达到阻塞效果。

阻塞队列主要用于“生产者消费者模型”,是实际开发中常用到的,下面我就来介绍它的用法。


2. 生产者消费者模型

什么是生产者消费者模型?从字面上来看,前者是生产者,后者是消费者。

因此,生产者与消费者之间进行交互需要一个中间平台,这个平台就是阻塞队列,如果没有中间平台交易就会产生一定风险、效率也会降低很多。

生产者消费者体现:过年大家都包饺子,假设一家有三个人员,人员1 擀饺子皮,擀完后放在砧板上,人员2 和 人员3 负责包饺子。这样一个例子中 人员1 就是生产者,砧板就是平台,人员2 和 人员3 是消费者。如果三个人员自己擀皮自己包,这样的效率是非常低的!(只有一个擀面杖、无砧板情况下)

中间平台优点体现:假如,有两个服务器它们直接进行交互。服务器1挂了,紧接着服务器2也挂了。因此,我们需要一个中间平台(阻塞队列),连接这两个服务器并进行交互。这样无论那一个服务器挂了也不影响另一个服务器。

生产者消费者模型的优点有很多,但最突出了有两点:解耦合削峰填谷。请看下方讲解。


2.1. 解耦合

大家都听过高内聚低耦合这个概念,在此我来做个解释:

何为内聚,举个例子:在快递站拿快递,我们可以根据货物号来快速的找到想要的物品,这就是高内聚

但某一天,快递站来了个怪人,他在找快递的过程中把每个拿起来的快递都随意放在其他位置。因此别人再去找自己的快递时就不能快速的找到自己的快递了,这就是低内聚的一个体现。

在 Java 中高内聚主要体现在代码的条理性,相关联的代码很好的放在一起。低内聚则是相关联的代码没有放在一起,东一块、西一块。

何为耦合主要体现一个关联性。也是举个例子:假设我的亲人生病住院了,我会放下手中的一切去好好照顾他/她,哪怕对我现实生活影响很大,我也义无反顾。这样的行为就是高耦合的。

但我的女神生病了,她发了个朋友圈。由于我和她只是“朋友圈点赞之交”,我只会给她点个赞并且评论句多喝热水。因为她生病了对我的影响是很低的,所以可以称为低耦合

耦合高,在 Java 主要体现在多个模块之间的关联,关联越强耦合越高,关联越弱耦合越低。


回归正题,阻塞队列的解耦合主要体现在多个线程之间进行交互。如以下例子:

在上、下图中,A、B、C是我们的业务服务器,会经常更改代码, 因此会经常出现 bug 就容易挂。通过消费者模型就能很好的避免这个问题。

当然,阻塞队列服务器也会挂,但相对于ABC业务服务器来说挂的机率较小。 


2.2 削峰填谷

三峡大坝利用的就是削峰填谷机制,有效缓解了电力系统在高峰期的压力和在低峰期的浪费现象。

当电力系统电力值达到高峰时,三峡大坝则会把部分的水存储在水库里面,只放出适合的水流量,减少并调节电力系统的负荷,有效缓解电力系统在高峰期的浪费现象。

当电力处于低峰期时也就是电力供给不足的情况,三峡大坝会把水库里存储的水给放出来,通过电站的发电量、水库的排水等措施,缓解了电力系统在低峰期的电力不足。

上述例子就是削峰填谷的一个简单理解,在 Java 中阻塞队列就能达到削峰填谷的功能。

当服务器与服务器之间进行交互常常是以一个很平缓的速率进行的,但某一时刻突然达到了一个峰值。

这个时候阻塞队列就能把峰值带来的压力给顶下来,让服务器之间还是以平稳的速率进行交互。

如:服务器A 作为生产者,服务器B 作为消费者,服务器A 最高可达到 1秒3万 次的速率,服务器B 最高只能 1秒1万 次这时候就会出现下图这样的问题。

上图中 服务器A 作为生产者、服务器B 作为消费者。当 服务器A 收到的请求多了。回复给阻塞队列的内容也变多了。

但 服务器B 最多能接受 1秒1万 次的数据。因此,阻塞队列就会把多的请求存储下来并按照 1秒1万 次的速率给 服务器B 传输数据,这样就不会导致 服务器B 崩溃。

以上的三峡大坝、服务器交互的例子就是对削峰填谷进行的一个讲解,当然比较浅显。具体代码的实现,请看下方讲解。


2.3 生产者消费者案例

生产者消费者主要体现一个线程生产,一个线程消费。如下代码:

public static void main(String[] args) {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        //消费者
        Thread thread1 = new Thread(()->{
            while (true) {
                try {
                    int value = blockingDeque.take();
                    System.out.println("消费者: "+value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();//启动线程1

        //生产者
        Thread thread2 = new Thread(()->{
            int value = 1;
            while (true) {
                try {
                    blockingDeque.put(value);
                    System.out.println("生产者: "+value);
                    Thread.sleep(1000);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();//启动线程2
    }

运行后打印:

以上代码不难看懂,主要用到阻塞队列的 takeput 方法。生产者 thread2 使用 put 方法生产元素,消费者 thread1 使用 take 方法消费元素。

注意,在线程内调用 take 或put 方法,都得 try/catch InterruptedException 这个异常。我们直接Alt+Enter take 或 put方法即可。


3. 阻塞队列生产者消费者模型的实现

使用阻塞队列实现生产者消费者模式过程如下:

首先我们要让这个队列循环下去,如何让一个队列循环下去,最好实现方法就是使用循环队列。

设计中我们可以用 head 作为队头元素下标、tail 作为队尾元素下标、size 作为当前元素的个数。

head 等于 tail 的时候证明是初始状态(队列空),或者是队列已满。因此,有以下几点注意事项:

入队列:

  • 当 size 等于队列长度时,证明队列已满,此时不能插入数据。
  • 当 tail 等于队列长度时,tail 置为0,从第一个位置开始插入元素。

出队列:

  • 当 size 等于 0 时,证明队列已空,此时不能出数据。
  • 当 head 等于队列长度时候,head 置为 0 ,从第一个元素开始出元素。

当然,为了达到阻塞的效果,在队列满状态空状态的方法里面使用 wait 方法造成阻塞状态。在插元素方法里面里面 notify 唤醒队列空时的阻塞状态,在拿元素里面 notify 唤醒队列满时的阻塞状态。

具体代码实现如下:

class MyBlockingQueue {
    int [] array = new int[100];//定义一个数组为队列
    int head = 0;//队头下标
    int tail = 0;//队尾下标
    int size = 0;//元素个数

    //模拟实现 put 方法
    synchronized public void put(int value) throws InterruptedException {
        if (size == array.length) {
            this.wait();//队列已满设为阻塞状态
        }
        array[tail] = value;//把value值放在数组对应下标中
        tail++;//队尾下表自增
        size++;//元素个数自增
        if (tail == array.length) {
            tail = 0;//队尾下标重置为0
        }
        this.notify();//唤醒队列空的阻塞状态
    }

    //模拟实现 take 方法
    synchronized public int take() throws InterruptedException {
        if (size == 0){
            this.wait();//队列已空设为阻塞状态
        }
        int value = array[head];//队头元素负责个value
        head++;//队头下标往后自增
        size--;//元素个数自减
        if (head == array.length) {
            head = 0;//队头下标置为0
        }
        this.notify();//唤醒队列满的阻塞状态
        return value;//返回队头元素
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        //生产者
        Thread thread1 = new Thread(()-> {
            int i = 1;
            while (true) {
                try {
                    System.out.println("生产者: "+i);
                    myBlockingQueue.put(i);
                    i++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        //消费者
        Thread thread2 = new Thread(()-> {
            while (true) {
                try {
                    int i = myBlockingQueue.take();
                    System.out.println("消费者: "+i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();
    }
}

运行后打印:

以上代码,我使用一个数组来模拟实现循环队列的这样更容易去理解。其他细节大家可以在代码中的注释进行理解。 队列已经循环队列不太熟悉朋友可以回头好好复习一下。

注意,一个队列不可能为空状态又为满状态,因此在上述代码中,notify 唤醒的都是对方的状态。这样一个阻塞队列生产者消费者模式就能很好的实现了。

另外,阻塞队列不存在线程安全问题,因为阻塞队列底层有加锁机制。因此,大家可以安心使用。

如果面试的时候,面试说:“请你写一个生产者消费者模型”。那么这个时候,你就可以利用上方代码进行拓展。


🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创作者、专家博主。

📒博客主页:这是博主的主页 

🗃️文章收录于:Java多线程编程 

🗂️JavaSE的学习:JavaSE 

🗂️Java数据结构:数据结构与算法 

 本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~

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

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

相关文章

12道c语言的课后习题!

1、计算n的阶乘&#xff08;1*2*3*4*5 n是个数&#xff0c;比如说乘到100&#xff1f;&#xff09; // 计算n的阶乘#include"stdio.h"int main() {int n 0;scanf("%d", &n);int i 0;int ret 1;for (i 1; i < n; i) {ret * i;}printf("…

技术分享| 融合会议协议大解密

anyRTC的融合会议解决方案中&#xff0c;支持H.323协议、SIP协议、GB28181国标协议、私有协议等等&#xff0c;实际在跟客户沟通时&#xff0c;我们常会被问到到底是SIP还是H.323好&#xff1f;客户前期已经建设了视频会议系统&#xff0c;有H.323的、有腾讯会议/Zoom这种互联网…

ESLint驼峰命名法规则校验关闭

目录 1、简单介绍ESLint1.1 ESLint是什么1.2 ESLint的好处1.3 ESLint的规范 2、 驼峰校验提示3、解决方案 1、简单介绍ESLint 1.1 ESLint是什么 ESLint 是一个代码检查工具&#xff0c;用来检查你的代码是否符合指定的规范 例如: 的前后必须有一个空格例如: 函数名后面必须…

郑州市元宇宙产业发展实施方案发布,中创助力元宇宙创新建设!

如果说2022年的“元宇宙”赛道&#xff0c;尚是以Meta为首&#xff0c;而今&#xff0c;政府也纷纷参与其中。 数据显示截至目前&#xff0c;国内已有至少30个地区&#xff08;涉及10个省级行政区&#xff09;颁布了元宇宙相关的支持性政策或征求意见稿&#xff0c;政策文件名中…

神策「数据闭环解决方案」重磅上线,3 大方向助推业务起飞

在通过数据驱动业务增长的落地实践中&#xff0c;企业大多面临以下困扰&#xff1a; 数据源多且杂乱&#xff0c;该如何从 0 开始积累数据助力业务决策&#xff1f; 如何借助数据的力量&#xff0c;洞察业务关键增长点&#xff1f; 如何提升运营效率&#xff1f;怎样提高用户的…

STM32——关于GPIO讲解及标准库应用(基础篇)

简介&#xff1a; STM32是一系列基于ARM Cortex-M内核的32位微控制器。该系列微控制器广泛应用于计算机、通讯、工业自动化、消费电子、汽车电子、医疗仪器及家庭电器等领域。该系列控制器具有高性能、低功耗、智能化等特点。其中&#xff0c;GPIO就是STM32控制器中的一…

Stack 栈的实现与应用

目录 1. 概念 2. 常用的栈的方法 2.1 方法 2.2 代码 3. 自己实现栈 3.1 构造MyStack 3.2 push() 3.3 ensureCapacity&#xff08;&#xff09; 3.4 pop() 3.5 peek() 3.6 empty() 3.7 szie() 4. 栈的应用 1. 概念 栈&#xff08;Stack&#xff09;是一种数据结构&…

02_stack栈

1. Stack 栈是什么&#xff1f; 栈也叫栈内存&#xff0c;主管Java程序的运行&#xff0c;是在线程创建时创建&#xff0c;它的生命期是跟随线程的生命期&#xff0c;线程结束栈内存也就释放&#xff0c;对于栈来说不存在垃圾回收问题&#xff0c;只要线程一结束该栈就Over&…

LabVIEWCompactRIO 开发指南37 在第三方模拟器中执行

LabVIEWCompactRIO 开发指南37 在第三方模拟器中执行 如果需要验证时序和功能&#xff0c;在将LabVIEW FPGA VI编译到硬件之前&#xff0c;可以与三款第三方仿真器进行交互&#xff1a;MentorGraphicsModelSim&#xff08;LabVIEW2013及更早版本&#xff09;、MentorGraphics…

【MySQL】MySQL主从同步延迟原因与解决方案

文章目录 一、MySQL数据库主从同步延迟产生的原因二、关于DDL和DML三、主从延时排查方法四、解决方案3.1 解决从库复制延迟的问题&#xff1a;3.2 MySql数据库从库同步其他问题及解决方案 一、MySQL数据库主从同步延迟产生的原因 MySQL的主从复制都是单线程的操作&#xff0c;…

JS CSS 关于 Shadow dom 的用法

一、什么是 Shadow DOM 你是否好奇过&#xff0c;浏览器自带的元素的样式是如何实现的&#xff0c;例如 video、input &#xff0c;又或者在某些网站中看到一些非浏览器自带且没见过的元素&#xff1f; 如果你打开 F12 查看定位该元素的信息&#xff0c;你会发现啥都没看到&am…

Spring Cloud Alibaba 完整使用及与各中间件集成

目录 前言 官方中文文档 使用版本 spring 中间件 使用到的组件与功能 环境安装 虚拟机 nexus nacos 集成过程 工程搭建 父工程搭建 子工程 服务集成 nacos 配置文件&#xff1a; 服务注册与发现-discovery 服务注册 启动 服务发现 测试 配置管理-config…

网络安全是一个好专业吗?

前言 网络安全作为一个专业领域&#xff0c;在当今数字时代正变得越发重要和关键。无论是企业还是个人&#xff0c;面对着越来越频繁的网络威胁和攻击&#xff0c;网络安全领域的专业人员扮演着至关重要的角色。那么&#xff0c;从一个资深网安工程师的角度来看&#xff0c;网…

nest context.switchToHttp().getRequest() 获取不到user

把你该死的Public注释掉&#xff0c;这玩意开了后不走JWT验证token&#xff0c;所以拿不到那该死的user。

pdf怎么合并在一起?软件操作更高效

PDF格式已经成为了许多文档和表格的首选格式。然而&#xff0c;当你需要合并多个PDF文件时&#xff0c;可能会遇到一些麻烦&#xff0c;在本篇文章中&#xff0c;我们将向您介绍一种简单易用的方法来合并PDF文件。 以下是可以用来合并PDF文件的软件&#xff1a; - PDF转换器&a…

当你在浏览器中输入 URL 时会发生什么?

下面的图解说明了步骤。 1.Bob 在浏览器中输入 URL 并按下 Enter。在这个例子中&#xff0c;URL 由 4 部分组成&#xff1a; &#x1f539; 协议 - http://. 这告诉浏览器使用 HTTP 发送连接到服务器。 &#x1f539; 域名 - example.com. 这是站点的域名。 &#x1f539; 路径…

C++STL— priority_queue的使用与模拟实现

priority_queue的使用 priority_queue的介绍 优先级队列默认使用vector作为其底层存储数据的容器&#xff0c;在vector上又使用了堆算法将vector中的元素构造成堆的结构&#xff0c;因此priority_queue就是堆&#xff0c;所有需要用到堆的位置&#xff0c;都可以考虑使用prio…

极米十年巅峰之作极米Z7X,能带走的百吋大屏

近年来&#xff0c;随着人们娱乐消费的升级&#xff0c;家用投影仪消费市场不断扩大&#xff0c;增长速度也非常可观。据IDC最新发布的数据显示&#xff0c;2022年中国投影机市场总出货量505万台&#xff0c;同比增长7.4%。其中&#xff0c;智能投影机市场&#xff08;搭载有OS…

ChatGPT:免费可用 ChatGPT 网页版(免登录、免注册、不限制使用次数)

一、什么是chatgpt ChatGPT是一种基于GPT-3(Generative Pre-trained Transformer 3)的聊天机器人。GPT-3是由OpenAI公司开发的自然语言处理模型&#xff0c;具有非常强的语言理解和生成能力。 ChatGPT使用了GPT-3的预训练模型&#xff0c;在此基础上进行了微调和优化&#xf…

机器学习:基于Apriori算法对中医病症辩证关联规则分析

系列文章目录 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&#x1f4d2;关注…