阻塞队列(超详细易懂)

目录

一、阻塞队列

1.阻塞队列概述

2.生产者消费者模型

3.阻塞队列的作用

4.标准库中的阻塞队列类

5.例子:简单生产者消费者模型

二、阻塞队列模拟实现

1.实现循环队列(可跳过)

1.1简述环形队列

1.2代码实现

2.实现阻塞队列

2.1实现思路

2.2代码实现

2.3代码解析

①wait和notify的使用,实现自动阻塞和解阻塞

②while循环判断,线程安全的铜墙铁壁

2.4纯享版代码实现(无注释)


一、阻塞队列

1.阻塞队列概述

阻塞队列是一种特殊的队列,同样遵循“先进先出”的原则,支持入队操作和出队操作。在此基础上,阻塞队列会在队列已满或队列为空时陷入阻塞,使其成为一个线程安全的数据结构,它具有如下特性:

  • 当队列已满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列为空时,继续出队列也会阻塞,直到有其他线程向队列中插入元素。

2.生产者消费者模型

生产者消费者模型有两种角色,生产者和消费者,两者之间通过缓冲容器来达到解耦合的效果。类似于厂商和客户与中转仓库之间的关系,如下图:

厂家生产的商品堆积在中转仓库,当中转仓库满时,入仓阻塞,当中转仓库为空时,出仓阻塞。通过上述结构,生产者和消费者摆脱了“产销一体”的运作模式,即解耦合。同时,无论是客户需求暴增,还是厂家产量飙升,都会被中央仓库协调,避免突发情况导致结构崩溃。

同理,根据生产者消费者模型,我们将线程带入到消费者和生产者的角色,阻塞队列带入到缓冲空间的角色,一个类似的模型很容易就搭建起来了。

所以说,阻塞队列对生产者消费者模型是相当重要的。

3.阻塞队列的作用

①解耦合

作为生产者消费者模式的缓冲空间,将线程(其他)之间分隔,通过阻塞队列间接联系起来,起到降低耦合性的作用,这样即使其中一个挂掉,也不会使另一个也跟着挂掉。

②削峰填谷

因为阻塞队列本身的大小是有限的,所以能起到一个限制作用,即在消费者面对突发暴增的入队操作,依然不受影响。

如电商平台在每年双十一时都会出现请求峰值的情况,如下(杜撰):

而假设电商平台对请求的处理流程是这样的:

因为处理请求需要消耗硬件资源,如果没有消息队列,面对双十一这种请求暴增的情况,请求处理服务器很可能就直接挂掉了。

而有了消息队列之后,请求处理服务器不必直接面对大量请求的冲击,仍旧可以按原先的处理速度来处理请求,避免了被冲爆,这就是‘削峰’。

没有被处理的请求也不是不处理了,而是当消息队列有空闲时再继续流程,即高峰请求被填在低谷中,这就是‘填谷’。

经过‘削峰填谷’之后的请求处理曲线就(大致)变成了下图:

4.标准库中的阻塞队列类

类名说明
LinkedBlockingQueue<>基于链表的阻塞队列(常用)
LinkedBlockingDeque<>基于链表的双端阻塞队列
LinkedTransferQueue<>基于链表的无界阻塞队列
ArrayBlockingQueue<>基于顺序表的阻塞队列
PriorityBlockingQueue<>带有优先级功能的阻塞队列
方法    解释
void put(E e)带有阻塞特性的入队操作方法(常用)
E take() 带有阻塞特性的出队操作方法(常用)
boolean offer(E e, long timeout, TimeUnit unit) 带有阻塞特性的入队操作方法,并且可以设置最长等待时间
E poll(long timeout, TimeUnit unit) 带有阻塞特性的出队操作方法,并且可以设置最长等待时间
public boolean contains(Object o)

 判断阻塞队列中是否包含某个元素

5.例子:简单生产者消费者模型

可调整生产时间和消费时间观察效果。

//模拟实现的阻塞队列
class MyBlockingQueue {private Object lock = new Object();private String[] elems = null;private int head = 0;private int tail = 0;private int size = 0;public MyBlockingQueue(int capacity) {elems = new String[capacity];}public void put(String elem) throws InterruptedException {synchronized(lock) {while(size == elems.length) {lock.wait();}elems[tail] = elem;tail++;if(tail >= elems.length) {tail = 0;}size++;lock.notify();}}public String tack() throws InterruptedException {String elem = null;synchronized (lock) {while(size == 0) {lock.wait();}elem = elems[head];head++;if(head == elems.length) {head = 0;}size--;lock.notify();}return elem;}
}public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue(10);//生产者线程Thread producerModel = new Thread(() -> {int number = 0;while(true) {try {queue.put("" + number);System.out.println("生产了 " + number++);Thread.sleep(400);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//消费者线程AThread consumerModelA = new Thread(() -> {String number;while(true) {try {number = queue.tack();System.out.println("A消费了 " + number);Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//消费者线程BThread consumerModelB = new Thread(() -> {String number;while(true) {try {number = queue.tack();System.out.println("B消费了 " + number);Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});producerModel.start();consumerModelA.start();consumerModelB.start();}
}

二、阻塞队列模拟实现

1.实现循环队列(可跳过)

在正式实现阻塞队列之前,我们需要先将普通循环队列这个框架搭建起来,再考虑为其加入阻塞队列的特性。

1.1简述环形队列

环形队列,也被称为循环队列,是一种特殊的线性数据结构。它的操作基于先进先出(FIFO)的原则,并且队尾被连接在队首之后以形成一个循环,可以使用数组或链表实现,这里采用数组。

  • 环形队列在逻辑上是环形的,但在物理上,它通常是通过一个定长的数组来实现的。
  • 环形队列的大小是确定的,一旦创建,它所能存放的元素个数就是固定的。
  • 先进先出,队列队首出队,队尾入队

1.2代码实现

class AnnularQueue {//String类型的数组,存储队列元素private String[] elems = null;//队首位置private int head = 0;//队尾位置private int tail = 0;//存储的元素个数private int size = 0;//构造方法,用于构建定长数组,数组长度由参数指定public AnnularQueue(int capacity) {elems = new String[capacity];}//入队方法public void put(String elem) throws InterruptedException {//当队列已满时,拒绝入队if(size == elems.length) {return;}//将元素存入队尾elems[tail] = elem;//存入后,队尾位置后移一位tail++;//实现环形队列的关键,超过数组长度后回归数组首位if(tail >= elems.length) {//回归数组首位tail = 0;}//存入后元素总数加一size++;}//出队方法public String tack() throws InterruptedException {String elem = null;//当队列为空时,拒绝入队,返回nullif(size == 0) {return elem;}//出队,取出队首值(不用置空,队尾存入时覆盖)elem = elems[head];//出队后,队首位置后移一位head++;//实现环形队列的关键,超过数组长度后回归数组首位if(head == elems.length) {//回归数组首位head = 0;}//存入后元素总数加一size--;//返回取出的元素return elem;}
}

2.实现阻塞队列

2.1实现思路

前面说到了“阻塞队列是一种特殊的队列,同样遵循‘先进先出’的原则,支持入队操作和出队操作”,实现一个只有入队和出队操作的队列很简单,关键在于如何将阻塞队列的特性加入进去,使其能够判断队列是否已满或是为空,进而阻塞等待。

判断是否未满很简单,只要在队列中定义一个size变量统计已存个数,当已存个数和队列长度相同时就为已满,size为0时队列为空。

阻塞等待也不难,只要引入锁,在入队、出队操作中使用wait和notify就可以了。

真正的难点在于,阻塞队列是适配于多线程程序的,必须要考虑到线程安全问题,而这一问题往往不好解决。

来,先看一下基于环形队列的简单阻塞队列到底是如何实现的吧。

2.2代码实现

测试可使用上文的例子‘简单生产者消费者模型’。

class MyBlockingQueue {//对象公用锁private Object lock = new Object();//String类型的数组,存储队列元素private String[] elems = null;//队首位置private int head = 0;//队尾位置private int tail = 0;//存储的元素个数private int size = 0;//构造方法,用于构建定长数组,数组长度由参数指定public MyBlockingQueue(int capacity) {elems = new String[capacity];}//入队方法public void put(String elem) throws InterruptedException {synchronized(lock) {//已满时入队操作阻塞while(size == elems.length) {lock.wait();}//将元素存入队尾elems[tail] = elem;//存入后,队尾位置后移一位tail++;//实现环形队列的关键,超过数组长度后回归数组首位if(tail >= elems.length) {//回归数组首位tail = 0;}//存入后元素总数加一size++;//当出队操作阻塞时,入队后为其解除阻塞//(入队后队列不为空了)lock.notify();}}//出队方法public String tack() throws InterruptedException {//存储取出的元素,默认为nullString elem = null;synchronized (lock) {//队列为空时出队操作阻塞while (size == 0) {lock.wait();}//出队,取出队首值(不用置空,队尾存入时覆盖)elem = elems[head];//出队后,队首位置后移一位head++;//实现环形队列的关键,超过数组长度后回归数组首位if(head == elems.length) {//回归数组首位head = 0;}//存入后元素总数加一size--;//当入队操作阻塞时,出队后为其解除阻塞//(出队后队列不满)lock.notify();}//返回取出的元素return elem;}
}

2.3代码解析

①wait和notify的使用,实现自动阻塞和解阻塞

首先看wait的位置:

判断条件很易懂,就是当队列已满(为空)时调用wait方法,使调用该方法的线程陷入阻塞,也就是说线程阻塞时,队列是一定陷入已满或为空状态的。

那么什么时候解除阻塞呢?当然是失去已满或为空状态的时候。

调用tack方法出队能够使队列留出位置,不再已满;调用put方法入队能够为队列存入元素,不再为空,两者相辅相成。

所以阻塞的put方法一定是要在别的线程调用tack方法,完成出队后才可能解除阻塞的;阻塞的tack方法也一定是要在别的线程调用put方法,完成入队后才可能解除阻塞的。

注意“可能”二字,因为阻塞的可能有很多线程,所以还要再参与lock锁竞争。

②while循环判断,线程安全的铜墙铁壁

代码中,while循环条件为判断队列状态是否已满(为空),若判断通过,则线程阻塞,等待状态解除。就直观效果而言,使用if语句和while循环的区别不大,但对于多线程程序,我们不得不多考虑一些。

在实现自动阻塞和解阻塞时,我们让put方法(入队操作)和tack方法(出队操作)互相为对方解除阻塞。

但是,put方法和stack方法中用的是同一把锁(lock),并且notify方法的机制是随机解除一个线程的阻塞,那么不管是put方法(入队操作)还是tack方法(出队操作)调用notify方法都可能反而为“同类”解除了阻塞,而我们的原意是要让他们互相解除阻塞。

下图示例就是一种bug可能,在队列容量为100,当前元素量为99的情况下,由于notify的随机唤醒机制,已满状态下的队列又进行了一次入队操作。(出队bug同理)

而使用while循环则不会出现这样的bug,由于wait方法在循环体内部,因此当阻塞结束后仍然会再次判断队列状态,即便再次堵塞后又出现同样的问题也没关系,大不了继续判断,至死方休。

③其他问题请留言😘

2.4纯享版代码实现(无注释)

class MyBlockingQueue {private static Object lock = new Object();private String[] elems = null;private int head = 0;private int tail = 0;private int size = 0;public MyBlockingQueue(int capacity) {elems = new String[capacity];}public void put(String elem) throws InterruptedException {synchronized(lock) {while(size == elems.length) {lock.wait();}elems[tail] = elem;tail++;if(tail >= elems.length) {tail = 0;}size++;lock.notify();}}public String tack() throws InterruptedException {String elem = null;synchronized (lock) {while(size == 0) {lock.wait();}elem = elems[head];head++;if(head == elems.length) {head = 0;}size--;lock.notify();}return elem;}
}

博主是Java新人,每位同志的支持都会给博主莫大的动力,欢迎留言讨论,如果有任何疑问,或者发现了任何错误,都欢迎大家在评论区交流“ψ(`∇´)ψ

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

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

相关文章

探索前端开发框架:React、Angular 和 Vue 的对决(一)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

webpack配置

一、很多基础方面的配置被vuecli所集成一般项目都是使用vuecli,不会真正的去从0-1进行webpack配置: 1、vuecli中的webpack基础配置: (1)入口文件默认在src/main;输出在dist; (2)集成了大量的插件和加载器:babel-loader 处理 JavaScript 文件、使用 css-loader 和 style-load…

基于控制台的购书系统(Java 语言实现)

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》|《数据结构与算法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢…

C++棋类小游戏2

今天给大家带来我花了1周时间自创的小游戏的升级版&#xff0c;博主还是一名小学生&#xff0c;希望大家提提意见。这是我写的最长的C代码&#xff0c;希望大家喜欢&#xff0c;不要抄袭&#xff0c;任何编译器都可以。 以前版本——C自创棋类小游戏-CSDN博客 C内容提示&…

苹果CMS挖片网升级版视频主题模版源码

自适应视频站正版高级挖片网收录模板&#xff0c;模板不错&#xff0c;是挖片网的升级版。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88799583 更多资源下载&#xff1a;关注我。

解决maven 在IDEA 下载依赖包速度慢的问题

1.idea界面双击shift键 2.打开setting.xml文件 复制粘贴 <?xml version"1.0" encoding"UTF-8"?> <settings xmlns"http://maven.apache.org/SETTINGS/1.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sc…

深度学习实战 | 卷积神经网络LeNet手写数字识别(带手写板GUI界面)

引言 在深度学习领域&#xff0c;卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是一种广泛应用于图像识别任务的神经网络结构。LeNet是一种经典的CNN结构&#xff0c;被广泛应用于基础的图像分类任务。本文将介绍如何使用LeNet卷积神经网络实现手写…

(已解决)spingboot 后端发送QQ邮箱验证码

打开QQ邮箱pop3请求服务&#xff1a;&#xff08;按照QQ邮箱引导操作&#xff09; 导入依赖&#xff08;不是maven项目就自己添加jar包&#xff09;&#xff1a; <!-- 邮件发送--><dependency><groupId>org.springframework.boot</groupId><…

vite打包原理

vite 工程化开发&#xff1a;打包工具 启动速度很快 核心原理还是webpack 把webpack封装了&#xff0c;把webpack对象封装了 和vue2整体结构几乎一致 webpack两种模式&#xff1a;开发&生产 代码打包编译&#xff0c;本地起一个web服务器实时预览编译后的结果 build 命令模…

2024.2.3

单向循环链表的头插 头删 尾插和尾删 //头结点插入 Linklist insere_element(Linklist head,datatype element) {Linklist screat();s->dataelement;if(NULLhead){heads;}else{Linklist phead;while(p->next!head){pp->next;}s->nexthead;heads;p->nexthead;}r…

太强了,AI数字人从制作到变现一次搞定

AI数字人从制作到变现 如果说GPT类大模型是我们人类的第二大脑&#xff0c;数字人就是我们人类在互联网上的第二个身体。随着 AI 的迅速发展&#xff0c;2024 年 AI 模型开始从大型语言模型向大型视觉模型转变。数字人技术作为其分支之一&#xff0c;正日益成为科技、娱乐、教…

Unity项目从built-in升级到URP(包含早期版本和2023版本)

unity不同版本的升级URP的方式不一样&#xff0c;但是大体流程是相似的 首先是加载URP包 Windows -> package manager,在unity registry中找到Universal RP 2023版本&#xff1a; 更早的版本&#xff1a; 创建URP资源和渲染器​​ 有些版本在加载时会自动创建&#…

ProcessSlot构建流程分析

ProcessorSlot ProcessorSlot构建流程 // com.alibaba.csp.sentinel.CtSph#lookProcessChain private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)throws BlockException {// 省略创建 Context 的代码// 黑盒…

Optimizer:基于.Net开发的、提升Windows系统性能的终极开源工具

我们电脑使用久了后&#xff0c;就会产生大量的垃圾文件、无用的配置等&#xff0c;手动删除非常麻烦&#xff0c;今天推荐一个开源工具&#xff0c;可以快速帮助我们更好的优化Windos电脑。 01 项目简介 Optimizer是一个面向Windows系统的优化工具&#xff0c;旨在提升计算机…

vulhub中spring的CVE-2022-22947漏洞复现

Spring Cloud Gateway是Spring中的一个API网关。其3.1.0及3.0.6版本&#xff08;包含&#xff09;以前存在一处SpEL表达式注入漏洞&#xff0c;当攻击者可以访问Actuator API的情况下&#xff0c;将可以利用该漏洞执行任意命令。 参考链接&#xff1a; https://tanzu.vmware.c…

【用Unity开发一款横板跳跃游戏部分需要学习的技术点指南】

*** 用Unity开发一款横板跳跃游戏部分需要学习的技术点指南 空洞骑士是一款基于横板平台跳跃的传统风格2D动作冒险游戏&#xff0c;庞大的游戏世界交错相通&#xff0c;玩家控制小虫子去探索幽深黑暗的洞穴&#xff0c;成为了一代人茶余饭后的惦念&#xff0c;深受玩家喜爱。 …

MySQL全表扫描:性能杀手的隐患与优化策略

MySQL全表扫描&#xff1a;性能杀手的隐患与优化策略 MySQL数据库作为常用的关系型数据库管理系统之一&#xff0c;全表扫描问题一直困扰着开发者。本文将深入剖析MySQL全表扫描的原理、其对性能的严重影响&#xff0c;同时提供一系列优化策略&#xff0c;助您高效应对MySQL性能…

ctfshow——文件包含

文章目录 web 78——php伪协议第一种方法——php://input第二种方法——data://text/plain第三种方法——远程包含&#xff08;http://协议&#xff09; web 78——str_replace过滤字符php第一种方法——远程包含&#xff08;http://协议&#xff09;第二种方法——data://&…

070:vue中provide、inject的使用方法(图文示例)

第070个 查看专栏目录: VUE 本文章目录 示例背景示例效果图示例源代码父组件代码子组件代码孙组件代码 基本使用步骤 示例背景 本教程是介绍如何在vue中使用provide和inject。在 Vue 中&#xff0c;provide 和 inject 是用于实现祖先组件向后代组件传递数据的一种方式。 在这个…

Docker存储空间清理

不知不觉服务器存储空间被Docker掏空了… 查看Docker空间占用情况 使用docker system df命令&#xff0c;可以加 -v 查看详情 清理Docker不需要的内容 使用docker system prune -a命令清理Docker 所有停止的容器所有没有被使用的networks所有没容器的镜像所有build cache …