初始JavaEE篇——多线程(5):生产者-消费者模型、阻塞队列

在这里插入图片描述

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程程(ಥ_ಥ)-CSDN博客

所属专栏:JavaEE

文章目录

  • 阻塞队列
  • 生产者—消费者模型
    • 生产者—消费者模型的优势:
    • 生产者—消费者模型的劣势:
  • Java标准库中的阻塞队列:
  • 模拟实现阻塞队列:

前面我们学习多线程的经典案例之一:饿汉模式与懒汉模式。两者的区别是创建类的实例的时机不同,前者是迫不及待的去创建类的实例,而后者是迫不得已去创建类的实例。这样就导致了前者在 get 方法中只有"读"操作,不会造成线程安全问题,而后者会出现线程安全问题。最后经过我们的不断深入探索并解决了其中的问题。首先是进行加锁操作,避免了修改操作原子性,其次是加了 if 判断语句,避免了不必要的加锁,从而导致的性能下降,最后,针对指令重排序的问题,在引用变量中加上了 volatile 关键字。如果想更加深入了解,可以点击下面的链接:饿汉模式、懒汉模式、指令重排序等

阻塞队列

现在我们来学习另外一个经典的案例:阻塞队列。
阻塞队列是属于队列的一种,但是和普通的队列相比,它具有以下的特性:
1、它具备线程安全的特点,即使在多线程的环境下,也是可以正常使用的。
2、阻塞特性:1)当队列为空时,如果再去队列中取元素的话,会发生阻塞,直至队列不为
空;2)当队列满了时,如果再去队列中插入元素的话,也会发生阻塞,直至队列不为满。

生产者—消费者模型

阻塞队列的主要应用场景是:“生产者—消费者模型”。那什么是生产者,什么又是消费者呢?简单理解就是,生产者与消费者之间是通过某种资源进行来进行交互的。生产者,就是生产这个资源的,而消费者,就是消耗这个资源的。
例如,在我们日常中,最常见的就是包饺子,包饺子需要擀面皮的人、包饺子的人、放面皮的布。(肉已经被绞肉机给搞好了) 这里就是一个经典的"生产者一消费者模型"。
生产者:擀面皮的人、消费者:包饺子的人、阻塞队列:放面皮的布。这里生产者与消费者进行交互的就是"面皮"这种资源。
我们在日常生活中,有两种包饺子的方式:
1、家里面几个人全部一起参与包饺子的全过程。即每个人都需要 擀面皮、包饺子。而擀面杖只有一个,那么当一个人在进行擀面皮时,另外几个人都得阻塞等待,当这个人把面皮给擀完之后,才会释放,这样下一个人才有机会去使用。这个擀面杖就是我们前面学习的锁。
上面的方式,我们会发现一个很大的缺陷:当其中一个人在生产面皮时,其余的人得阻塞等待,也就是有空闲时间。这对于计算机来说,简直就是浪费,因此下面这种方式更为合理。
2、一个人专门擀面皮,另外的人负责包饺子,这样就不会导致生产者或者消费者会出现空闲的情况(生产速度与消费速度是一致的)。

生产者—消费者模型的优势:

1、解耦合:
生产者与消费者避免了直接交互,而是通过阻塞队列来进行交互,这样有利于代码的解耦合,使得后期的维护成本变低。
2、削峰填谷:
当 生产者—消费者模型 应用于两个服务器时,就可以达到削峰填谷的效果。

在这里插入图片描述

因此为了避免上述的情况,我们需要对用一个阻塞队列来处理这种"突然的大量请求的情况"。例如,学校选课的时候,通常就会出现这样的情况。
解决方法:使用一个阻塞队列来充当缓冲的作用。当A服务器突然接收到有大量的请求时,这个阻塞队列便会接收这些请求,但是还是以平常的速度给到B服务器,这样B服务器还是会正常运行,这就是 “削峰”。当这个峰值过去之后,就是平常少量的请求,而阻塞队列这时候就会来处理在高峰期接收的请求,这样B服务器还是以平常的速度在处理请求,这就是"填谷"。阻塞队列通过降低高峰期的发送请求,而是在低谷期来处理,这样B服务器就是以一个平均的速度在处理请求。
注意:
1、阻塞队列通常可以接收并存放很多的请求。
2、高峰期比较短,所以阻塞队列一般不会出现满的情况。

生产者—消费者模型的劣势:

1、引入阻塞队列之后,整体的结构相交以前更为复杂了,同时也需要更多的机器进行部署,使生产环境的结构更复杂,同时管理起来也更为麻烦了。
2、效率也有一定的影响。之前是A服务器和B服务器直接进行交互,现在多了个阻塞队列,消息的传递所消耗的时间也变多了。

Java标准库中的阻塞队列:

Java的标准库中提供的阻塞队列是:BlockingQueue。

在这里插入图片描述

我们在日常的开发中,主要就是使用:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue。下面是使用的示例:

在这里插入图片描述

public class Test {public static void main(String[] args) throws InterruptedException {BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(100);// 在多线程中,队列的插入方法要用put。因为其带有阻塞功能,且线程安全queue.put(1);queue.put(2);queue.put(3);queue.put(4);// 同样多线程中的删除也要用takeint n = queue.size();for (int i = 0; i < n; i++) {System.out.print(queue.take()+" "); // 1 2 3 4}}
}

我们现在可以去看一下阻塞功能。

public class Test {public static void main(String[] args) throws InterruptedException {BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);// 在队列为空的情况下,尝试去取元素System.out.println(queue.take()); // 由于是单线程,因此会一直阻塞,即死等。queue.put(1);// 在队列为满足,尝试去插入新元素queue.put(2);}
}

同样下面去尝试插入新元素时,也是会发生阻塞等待的,也是死等的情况。

public class Test {public static void main(String[] args) {int n = 1;System.out.println("队列的总容量:"+n);BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(n);System.out.println("队列此时的容量:"+queue.size());Thread t1 = new Thread(()-> {try {System.out.println("阻塞队列为空,尝试取出元素,等待其他的线程插入元素");queue.take();System.out.println("成功取出元素~");} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(()->{try {for (int i = 0; i < 3; i++) {Thread.sleep(1000); // 确保t1线程先执行到take方法,并放慢让我们观察System.out.println("正在尝试插入第" + (i + 1) + "个元素");queue.put(i);System.out.println("插入成功~");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();}
}

运行结果:
在这里插入图片描述

模拟实现阻塞队列:

要求:我们主要是实现队列的 put方法、take方法、size方法即可。

思路:put、take方法都需要保证线程安全和阻塞的特性。
线程安全,我们直接对代码进行加锁操作即可;
阻塞特性:当队列为满时,要阻塞到其他线程使用掉其中的对头元素,即得等待其他线程调用take方法来唤醒当前因队列满而造成的阻塞,这也就需要用到我们前面学习的wait 和 notify 方法。

阻塞队列代码:

public class MyBlockingQueue {// 基于数组去模拟实现private static int[] array = null;private static int usedSize = 0; // 元素个数private int head = 0; // 头指针private int tail = 0; // 尾指针public MyBlockingQueue() {array = new int[10];}public MyBlockingQueue(int capacity) {if (capacity < 0) {throw new RuntimeException();} else if (capacity >= Integer.MAX_VALUE) {array = new int[Integer.MAX_VALUE];} else {array = new int[capacity];}}// put方法public void put(int x) throws InterruptedException {synchronized (this) { // 保证线程安全while (usedSize >= array.length) { // 满足阻塞队列的特性// 阻塞等待this.wait(); // 等待其他线程取出元素,使队列不为满}array[tail] = x;tail = (tail+1) % array.length; // 这里是采用循环队列的方式usedSize++;this.notify(); // 唤醒空的阻塞}}public int take() throws InterruptedException {synchronized (this) { // 保证线程安全while (usedSize <= 0) { // 满足阻塞队列的特性// 阻塞等待this.wait(); // 等待其他线程插入元素,使队列不为空}int ans = array[head];head = (head+1) % array.length;usedSize--;this.notify(); // 唤醒满的阻塞return ans;}}public int size() {return usedSize;}
}

测试代码:

public class Test {public static void main(String[] args) throws InterruptedException {int n = 1;MyBlockingQueue queue = new MyBlockingQueue(n);System.out.println("队列的总容量为:"+n);System.out.println("队列当前的容量为:"+queue.size());Thread t1 = new Thread(()->{try {Thread.sleep(500);System.out.println("队列为空,阻塞等待别的线程插入数据");queue.take();System.out.println("取出元素~");} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(()->{for (int i = 0; i < 3; i++) {try {Thread.sleep(1000);System.out.println("正在尝试插入第"+(i+1)+"个元素");queue.put(i);System.out.println("插入成功~");} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

运行结果:

在这里插入图片描述

注意:
1、我们使用加锁操作,是为了避免出现下面这种情况:一个线程在修改,另一个线程在读取,把数据修改之后,可能会造成另一个线程执行有误,因此我们得对代码进行加锁操作,是同一时刻只能有一个线程去进行修改操作(读取操作是不会影响数据的),因此对于修改操作的代码,都得处于 synchronized 代码块中,而上述 put、take 方法的大部分代码都是修改操作,因此我们就将整个代码逻辑都置于 synchronized 代码块中了。

2、put、take 方法中之所以将判断阻塞的条件放到 while 循环中,是因为可能会出现下面这样的情况:有三个线程都是处于put方法的阻塞状态,而这时新来了一个执行take方法的线程,其会随机唤醒三个线程中的一个,当三个线程中,某个线程执行完 notify 方法之后,也会随机唤醒剩下的两个线程,但是此时这个唤醒操作不符合要求,因为我们是希望将处于take方法的阻塞线程所唤醒,因此这个是错误唤醒,所以我们要用 while 循环去再次线程判断到底是不是因为正常唤醒而被唤醒的。

好啦!本期 初始JavaEE篇——多线程(4):生产者-消费者模型、阻塞队列 的学习之旅 就到此结束啦!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用ChatGPT提升工作效率:从理论到实际应用

伴人工智能技术的迅速演进&#xff0c;像ChatGPT这类语言模型已成为提升工作效率的关键工具。这类模型不仅具备处理海量数据的能力&#xff0c;还能自动化许多日常任务&#xff0c;从而提高决策的准确性。本文将深入探讨如何在工作中利用ChatGPT等AI工具提升效率&#xff0c;涵…

如何修改网络ip地址:一步步指南‌

在当今这个数字化时代&#xff0c;网络已成为我们日常生活与工作中不可或缺的一部分。无论是浏览网页、在线办公还是享受流媒体服务&#xff0c;稳定的网络连接和适当的IP地址管理都是确保良好体验的关键。然而&#xff0c;出于隐私保护、绕过地理限制或测试网络环境等需要&…

ENSP (虚拟路由冗余协议)VRRP配置

VRRP&#xff08;Virtual Router Redundancy Protocol&#xff0c;虚拟路由冗余协议&#xff09;是一种用于提高网络可用性和可靠性的协议。它通过在多个路由器之间共享一个虚拟IP地址&#xff0c;确保即使一台路由器发生故障&#xff0c;网络依然能够正常运行&#xff0c;防止…

SpringCloud Alibaba-05 Seata分布式事务处理

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用&#xff0c;就会产生分布式事务问题。但是关系型数据库提供的能力是基于单机事务的&#xff0c;一旦遇到分布式事务场景&#xff0c;就需要通过更多其他技术手段来解决问题。 1.四大模式&#xff1a; Seata AT模式(主…

非线性数据结构之图

一、有向图&#xff08;Directed Graph&#xff09; 1. 定义 有向图是一个由顶点&#xff08;节点&#xff09;和有方向的边&#xff08;弧&#xff09;组成的图。在有向图中&#xff0c;每条边都有一个起点和一个终点&#xff0c;表示从一个顶点到另一个顶点的关系。 2. 特…

大数据之Hadoop集群

Hadoop集群介绍&#xff1f;Hadoop集群的优缺点及应用场景&#xff1f;Hadoop集群搭建&#xff1f;Hadoop架构&#xff1f; Hadoop集群介绍 Hadoop集群是由多台计算机&#xff08;节点&#xff09;组成的一个分布式计算系统&#xff0c;主要用于处理大规模的数据集。以下是对Ha…

云原生+AI核心技术&最佳实践

以下内容是我在陕西理工大学2023级人工智能专业和网络专业的演讲内容&#xff0c;分享给大家。 各位老师、同学们&#xff0c;大家好啊&#xff01;能在这里跟大家一起聊聊咱们计算机专业那些事儿&#xff0c;我真的觉得超级兴奋&#xff01; 首先&#xff0c;自我介绍一下&am…

数字信号处理Python示例(5)使用实指数函数仿真PN结二极管的正向特性

文章目录 前言一、二极管的电流-电压关系——Shockley方程二、PN结二极管正向特性的Python仿真三、仿真结果分析写在后面的话 前言 使用Python代码仿真了描述二极管的电流-电压关系的Shockley方程&#xff0c;对仿真结果进行了分析&#xff0c;说明在正向偏置区域&#xff0c;…

真·香!深度体验 zCloud 数据库云管平台 -- DBA日常管理篇

点击蓝字 关注我们 zCloud 作为一款业界领先的数据库云管平台&#xff0c;通过云化自治的部署能力、智能巡检和诊断能力、知识即代码的沉淀能力&#xff0c;为DBA的日常管理工作带来了革新式的简化与优化。经过一周的深度体验&#xff0c;今天笔者与您深入探讨 zCloud 在数据库…

ICPC区域赛成都站【赛后回顾+总结】

传送门 前言赛后总结赛后回顾赛后感悟 前言 首先&#xff0c;这是本人本赛季第一场XCPC区域赛&#xff0c;也是本人算竞生涯中第一场XCPC区域赛&#xff08;之前只打过邀请赛和省赛&#xff09;。 赛后总结 然后赛后总结一下&#xff1a;我队天崩开局&#xff0c;我队出师不利…

Linux和,FreeRTOS 任务调度原理,r0-r15寄存器,以及移植freertos(一)

目录、 1、r0-r15寄存器&#xff0c;保护现场&#xff0c;任务切换的原理 2、freertos移植 3、freertos的任务管理。 一、前言 写这篇文章的目的&#xff0c;是之前面试官&#xff0c;刚好问到我&#xff0c;移植FreeRTOS 到mcu&#xff0c;需要做哪些步骤&#xff0c;当时回…

如果 MySQL 主库出现了问题,从库该何去何从呢?

🚀 博主介绍:大家好,我是无休居士!一枚任职于一线Top3互联网大厂的Java开发工程师! 🚀 🌟 在这里,你将找到通往Java技术大门的钥匙。作为一个爱敲代码技术人,我不仅热衷于探索一些框架源码和算法技巧奥秘,还乐于分享这些宝贵的知识和经验。 💡 无论你是刚刚踏…

基于Matlab 模拟停车位管理系统【源码 GUI】

系统对进入停车位的车辆进行车牌识别&#xff0c;将识别出来的车牌号显示出来&#xff1b;然后对车主进行人脸识别&#xff0c;框出车主照片的人脸部分作为车主信息的标记&#xff0c;记录在系统库中。车辆在库期间&#xff0c;系统使用者可以随意查看车辆与车主信息的获取过程…

【docker】docker 环境配置及安装

本文介绍基于 官方存储库 docker 的环境配置、安装、代理配置、卸载等相关内容。 官方安装文档说明&#xff1a;https://docs.docker.com/engine/install/ubuntu/ 主机环境 宿主机环境 Ubuntu 20.04.6 LTS 安装步骤 添加相关依赖 sudo apt-get update sudo apt-get install…

【论文阅读笔记】Wavelet Convolutions for Large Receptive Fields

1.论文介绍 Wavelet Convolutions for Large Receptive Fields 大感受野的小波卷积 2024 EECV Paper Code 2.摘要 近年来&#xff0c;人们试图通过增加卷积神经网络&#xff08;ConvolutionalNeuralNets&#xff0c;CNNs&#xff09;的核尺寸来模拟视觉变换器&#xff08;V…

DFS求解迷宫最长移动路线

来源:十四届蓝桥杯STEMA考试Python真题试卷第二套编程第五题 本文给出了C++实现代码,介绍了 STL 中容器vector,pair,unordered_set 的应用,供信奥选手参考。迷宫类问题适合用DFS算法解决,本文最后总结了DFS算法的两种常见实现方式——递归实现、栈实现,应用场景——迷宫…

【react使用AES对称加密的实现】

react使用AES对称加密的实现 前言使用CryptoJS库密钥存放加密方法解密方法结语 前言 项目中要求敏感信息怕被抓包泄密必须进行加密传输处理&#xff0c;普通的md5加密虽然能解决传输问题&#xff0c;但是项目中有权限的用户是需要查看数据进行查询的&#xff0c;所以就不能直接…

SpringBoot新闻稿件管理系统:架构与实现

3系统分析 3.1可行性分析 通过对本新闻稿件管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本新闻稿件管理系统采用SSM框架&#xff0c;JAVA作为开发语…

光耦合器的关键作用和创新---腾恩科技

光耦合器或光隔离器已成为电路中必不可少的器件&#xff0c;它允许信号在无需直接电接触的情况下跨不同电压域传输。这种隔离能力对于保护低压元件免受高压电路的潜在损坏至关重要。本文将仔细研究光耦合器在当今技术中发挥的独特作用&#xff0c;并探讨其在各种应用中不断扩展…

HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac

寻找模拟器 背景&#xff1a; 运行的是h5&#xff0c;模拟器是网易MuMu。 首先检查一下是否配置dab环境&#xff0c;adb version 配置一下hbuilderX的adb&#xff1a; 将命令输出的路径配置到hbuilderx里面去&#xff0c;然后重启下HbuilderX。 开始安装基座…一直安装不…