JavaEE——阻塞式队列

news2025/1/12 1:54:11

文章目录

  • 一、阻塞式队列
  • 二、生产者消费者模型
    • 1.发送方和接受方之间的 “解耦合”
    • 2. “削峰填谷”保证系统稳定性
    • 3、代码实现阻塞式队列

一、阻塞式队列

阻塞式队列,顾名思义也是一个队列,这个队列遵循的是先进先出的原则。

这里简单提一个特殊的队列,这个队列是 PriorityQueue 优先级队列,这个队列不遵守先进先出的原则

阻塞队列,也是一个比较特殊的队列,虽然是先进先出的,但是带有特殊的功能,如下:

  • 如果队列为,执行出队列操作,就会阻塞。阻塞到另一个线程往队列里添加元素 (队列不空) 为止。
  • 如果队列了,执行入队操作,也会阻塞。阻塞到另一个线程从队列取走元素为止 (队列不满)

根据上面阻塞式队列的这些特性,从而衍生出了一个非常典型的应用场景“生产者消费者模型”。这是一种非常典型的开发模型。

二、生产者消费者模型

在这里,我们先举出一个简单的例子,就是每次过年时一家人坐在一起包饺子。
在日常包饺子我们会有下面两个经典的包法:

  1. 每个人都进行擀饺子皮,包饺子这两个操作。(此时,大家会竞争擀面杖,就会产生阻塞等待,影响效率)
  2. 一个人专门负责擀饺子皮,另外几人负责包饺子。(这样就不存在竞争的问题,包的效率就和擀的效率有很大关系)

不难发现,第 2 种情况更加符合实际,这种形式就是生产者消费者模型。负责擀饺子皮的就是生产者,负责包饺子的就是消费者
在这里,擀饺子皮的速度慢了,包的就得等,擀饺子皮的太快了,擀饺子皮的就等。

对于生产者消费者模型,可以带给我们非常重要的两个好处!!

1.发送方和接受方之间的 “解耦合”

所谓 “解耦合” ,就是降低程序和程序之间的关联性。
典型场景:服务器之间的相互调用。
如图:

在这里插入图片描述

  1. 首先用户发出充值操作,由 A 服务器接受
  2. 之后 A 服务器将请求传递给 B 服务器处理,之后将处理结果返回给 A 服务器,此时,可以视为 A 调用了 B

在上面的场景中,A 和 B 之间的耦合度是比较高的,A 要调用 B,A 就必须要知道 B 的存在,如果此时 B 出现问题,就很容易引起 A 的 bug!
此外,如果在 A 服务器和 B 服务器之间根据需要在加入 C 服务器。这就需要对 A 服务器进行修改更新,重新测试,重新发布,重新部署。 这样就麻烦很多了。。

针对上面的场景,使用生产者消费者模型,就可以很好的降低 耦合度。
如图:
在这里插入图片描述
此时,A 服务器和 B 服务器之间的耦合度就降低很多了。

  1. 用户发出操作传递到服务器 A,当 A服务器接收后就存放到阻塞队列中不与 B 服务器直接接触。
  2. B 服务器处理完相应操作后,也不直接反馈到 A 服务器,同样的存入到对应的阻塞队列中。

在修改后服务器与服务器之间的联系形式发生很大的改变。

  • A 不知道 B 的存在。A 只知道阻塞队列。(A 的代码中没有任何代码与 B 相关)
  • B 也不知道 A 的存在。B 也只知道阻塞队列。(B 的代码中也没有任何与 A 相关的代码)
  • 如果 B 出现故障对 A 没有任何影响。因为阻塞队列任然存在,A 任然可以给队列中插入元素,如果队列,就先进行阻塞。
  • 如果 A 出现故障对于 B 也没有任何影响。同样因为阻塞队列存在,B 仍然也可以从队列中获取元素。若队列,也就先进行阻塞。

总的来说,A 和 B 任何一方出现问题都不会对对方产生影响。
此时在要新增一个 C 作为消费者也是没有问题的。

2. “削峰填谷”保证系统稳定性

在日常生活中,我们一定知道水坝这个东西。
我们在上面介绍的阻塞队列就和水坝的作用十分相似。
对水坝而言:

  • 上游水量增加——关闸蓄水。
  • 上游水量减少——开闸放水。

对应到阻塞队列中:

  • 上游——用户端发送的请求
  • 下游——对应执行具体操作的服务器

所以上游用户的请求申请量的大小会随时冲击下游的各个服务器,阻塞队列在此就是起到水坝缓冲的作用。

3、代码实现阻塞式队列

我们知道,阻塞是队列仍然是队列的一种,所以,要自己实现一个阻塞队列就需要先设计出一个普通的队列。
队列基于两类,一种是基于数组,一种是基于链表,链表的实际是相对比较简单的,在这里,我来以循环数组的形式来给大家实现这个队列。

循环数组大致分为两类:

  1. 留出一个空间用于判断数组是否已满,如图在这里插入图片描述
  2. 不浪费这个空间,引入一个 size 元素来进行记录个数。
    在这里插入图片描述
    在这里我依照第二种情况的循环数组来实现队列。

到此,设计一个什么样的队列已经确定了,下面我们就需要确定阻塞队列都含有哪些方法。
对于阻塞队列主要方法有两个:1. 入队列 (put) 2. 出队列 (take) 这两个方法都是是带有阻塞功能的,知道上面的这些条件后,接下来就是通过代码来实现相关的操作了。

普通队列的实现

  1. 实现方法前准备
//不考虑泛型,直接使用 int 元素表示类型
class MyBlockingQueue {
//设定数组长度
    private int[] item = new int[1000];
    //设定头指针
    private int head = 0;
    //设定尾指针
    private int tail = 0;
    //记录数组长度
    private int size = 0;
}
  1. 实现 put 存放方法
    //实现 put 存入方法
    public void put(int value){
            //判断数组是否已经存满
            if (size == item.length){
                //队列满了,无法插入
                return;
            }
            item[tail] = value;
            tail++;
            //针对 tail 的情况
            //这里 tail 在上面已经 +1 所以式子中无需加 1.
            //(1) tail = tail % item.length;
            //(2)
            if(tail >= item.length){
                tail = 0;
            }
            size++;
    }

在上面代码中针对 tail 的情况,本人更推荐第二种,因为第二种更加易于理解,直接将 tail 从队列末尾跳转到数组队列头部,不易出错。

  1. 实现 take 获取方法
    //实现 take 拿取方法
    public Integer take(){
    //记录获取的数据
        int result = 0;
            //判断不能出队列的情况
            if(size == 0){
                //空队列
                return  null;
            }
            result = item[head];
            head++;
            //判断当 head 超出数组的情况
            if(head >= item.length){
                head = 0;
            }
            size--;
        return result;
}

到此,一个普通的循环队列实现完毕,现在只要在这个基础上添加阻塞条件,就形成了一个阻塞队列。

我们知道,阻塞功能就意味着要在多线程下使用,存在多线程就必然会有线程安全问题,所以就需要使用 synchronized 对可能被修改的变量进行加锁。

    //实现 put 存入方法
    public void put(int value) throws InterruptedException {
        //为了防止在多线程中产生不必要的修改,对元素进行加锁
        synchronized (this){
            //判断数组是否已经存满
            //使用 while 循环 反复判断是否满足条件
            while(size == item.length){
                //队列满了,无法插入
                //需要判断进行阻塞等到
                //return;
                this.wait();
            }
            item[tail] = value;
            tail++;
            //针对 tail 的情况
            //(1) tail = tail % item.length;
            //(2)
            if(tail >= item.length){
                tail = 0;
            }
            size++;

            //这里的 notify 是用来唤醒 take 中的 wait
            this.notify();
        }
    }

    //实现 take 拿取方法
    public Integer take() throws InterruptedException {
        int result = 0;
        synchronized (this){
            //判断不能出队列的情况
            while(size == 0){
                //空队列
                //当空队列时也需要等待
                //return  null;
                this.wait();
            }
            result = item[head];
            head++;
            //判断当 head 超出数组的情况
            if(head >= item.length){
                head = 0;
            }
            size--;
            //当出队列后进行 唤醒
            this.notify();
        }
        return result;
    }
}

加锁操作保证了代码中的各个变量不会出现被篡改的情况,关于 wait 和 notify 关键字就是实现阻塞等待操作。

这里再详细说明一下,wait 和 notify 是如何相互进行制约的,如图所示:

在这里插入图片描述

除此之外,代码中还有一点有变化,如图所示:
在这里插入图片描述
这里要提出一个问题,当 wait 被唤醒的时候,if 语句中的条件就一定不成立了吗? 以 put 为例,具体来讲,就是当 wait 被唤醒的时候 队列一定是不满的吗?

虽然说这样的可能性比较小,但是不能排除会有这样的可能,所以解决办法就是让其循环多次判断,观察是否满足条件!

这样,我们就自主实现了一个阻塞队列,下面通过代码进行验证一下。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();

        Thread t1 = new Thread(()->{
           //消费者线程
            while(true){
                int result = 0;
                try {
                    Thread.sleep(500);
                    result = queue.take();
                    System.out.println("消费:"+ result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
            //生产者线程
            int count = 0;
            while(true){
                try {
                    System.out.println("生产:"+ count);
                    count++;
                    queue.put(count);
                    //Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
    }
}

在这里设定了两个线程分别为 生产者 和 消费者,我们在其中设定 sleep 可以很清楚地观察到阻塞队列的运行情况,如图:
设定生产者生产较慢
在这里插入图片描述
我们会发现,生产和消费几乎是同步进行的,但是结果有个问题,元素还没有生产出来,为什么就出现消费了?
其实这也不难解释,因为我们没有在打印操作上设定优先顺序,所以,其实元素已经生产出来,但是由于抢占式执行,所以会有些不对应。

设定消费者消费较慢
在这里插入图片描述
我们会发现,在一瞬间就产生了很多元素,最后消费者才缓慢消费,最终达到一个平衡。

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

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

相关文章

chatgpt赋能Python-python_output用法

Python Output 用法介绍 Python 是一种非常流行的编程语言,其简单而有效的语法和丰富的功能集使其成为了各种应用程序和数据分析项目的首选工具。 Python 在输出方面有非常灵活的方式,本文将介绍 Python Output 的用法。 使用 print 函数输出 Python …

python爬虫之request库的使用(友好版)

以下所有爬取的网站都是可以爬取的,爬取时请先学学法律哦~ 如有侵权,私信删除~ 本章目录~。~ 一,request库简介: 二,requests使用方法 1.GET请求 1.1,发起一个get请求 1.2,利用GET请求发…

三、数据仓库实践-拉链表设计

1 写在开头的话 拉链表,学名叫缓慢变化维(Slowly Changing Dimensions),简称渐变维(SCD),俗称拉链表,是为了记录关键字段的历史变化而设计出来的一种数据存储模型,常见于…

蓝奏云软件库源码分享下载(后端源码)

正文: FreePlus后台管理系统是一个基于[Thinkphp]的后台管理系统,提供了基本的应用管理、用户管理 、卡密管理 、笔记管理 、邮箱管理 、商城管理 、论坛管理 、附件管理、软件库、工具箱等功能。#### 软件架构thinkphp5.1mysql实现#### 安装教程(php必…

Spark大数据处理讲课笔记--- RDD持久化机制

零、本讲学习目标 理解RDD持久化的必要性了解RDD的存储级别学会如何查看RDD缓存 一、RDD持久化 (一)引入持久化的必要性 Spark中的RDD是懒加载的,只有当遇到行动算子时才会从头计算所有RDD,而且当同一个RDD被多次使用时&#…

基于SpringBoot的家乡特色推荐系统的设计与实现

背景 设计一个家乡特色推荐系统,通过这个系统能够满足家乡特色文章的管理功能。系统的主要功能包括首页,个人中心,用户管理,文章分类管理,文章分享管理,系统管理等。 管理员可以根据系统给定的账号进行登…

堪比ChatGPT,Claude注册和使用教程

新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT资源 Claude简介 Claude是一款人工智能聊天机器人。主要有以下特征: 使用自己的模型与训练方法,而不是基于GPT-3等开源框架。模型采用Transformer编码器与解码器的结构,并使用对话上下文的双向…

Spark大数据处理讲课笔记---RDD容错机制

零、本讲学习目标 了解RDD容错机制理解RDD检查点机制的特点与用处理解共享变量的类别、特点与使用 一、RDD容错机制 当Spark集群中的某一个节点由于宕机导致数据丢失,则可以通过Spark中的RDD进行容错恢复已经丢失的数据。RDD提供了两种故障恢复的方式&#xff0c…

全国大学生数据统计与分析竞赛2021年【研究生组】-B题:“互联网+教育”用户消费行为分析预测模型(附获奖论文和python代码实现)

目录 摘要 1 问题重述 2 问题分析 3 符号说明 4 模型建立与求解 4.1 问题一 4.1.1 数据预处理 4.1.2 处理结果 4.2 问题二 4.2.1 城市分布情况 4.2.2 用户登录情况 4.3 问题三 4.3.1 模型建立 4.3.2 模型求解 4.3.3 模型优化 4.4 问题四 4.4.1 模型建立 4.4.…

Windows 编译 OpenCV 头疼 ? 已编译好的,你要不要吧

一、使用官方编译好的 【Qt】opencv源码&官方编译好的opencv在windows下使用的区别_外来务工人员徐某的博客-CSDN博客 官方替我们编译好了,可以直接拿来用,但是看到下面这两个文件夹就知道,官方是用msvc编译器编译的,所以还是…

2天搞定-从零开始搞-量化交易-Python 【案例A股量化交易】第一节

搭建windows电脑开发环境 一,下载并搭建python 环境 1:python 安装过程教程:https://blog.csdn.net/weixin_44727274/article/details/126017386 2:python 下载地址官网:https://www.python.org/downloads/windows/ (过程较慢耐心等待,多版本选择) 3:python 本人放…

chatgpt赋能Python-python_noj

Python NOJ - 一款适合Python学习者的在线编程环境 Python NOJ是一款在线的Python编程环境,其全称为Python Online Judge,是一款适合Python学习者使用的编程工具。接下来,我们将介绍其主要特点和优势,并探讨其与其他在线编程环境…

chatgpt赋能Python-python_nmpy

Python NumPy:提高数据科学和数学计算的效率 在数据科学和数学计算领域,Python一直是最受欢迎的语言之一。NumPy是一个优秀的Python库,它通过提供一个强大的多维数组对象和与之相关的各种函数,极大地提高了Python在数据科学和数学…

2022下半年上午题

2022下半年上午题 b b d a c d 在做加法前先用补码表示 c a d c a c b b 专利权需要申请,题目中没说公司申请了专利 c c 前向传播取大值 d 反向传播求关键路径 b b b d a c 先在前驱图中把信号量定义下去 然后定义p,v操作 然后直接看图 1:从p1出来…

Spark大数据处理讲课笔记-- 理解RDD依赖

零、本讲学习目标 理解RDD的窄依赖理解RDD的宽依赖了解两种依赖的区别 一、RDD依赖 在Spark中,对RDD的每一次转化操作都会生成一个新的RDD,由于RDD的懒加载特性,新的RDD会依赖原有RDD,因此RDD之间存在类似流水线的前后依赖关系…

CANFDCAN协议对比 - 基础介绍_02

目录 四、CAN和CANFD区别 1、保留位 2、FDF-FD格式 五、高速传输机制 1、位速率切换 (Bit Rate Switch) 2、波特率5MBit/s 3、BRS和CRC界定符之间采用更高的波特率 六、CANFD数据场 1、经典CAN中DLC:9种可能的长度 2、CANFD中DLC:16种可能的长…

ChatGPT你真的玩明白了?来试试国内免费版的ChatGPT吧!

文章目录 一、什么是ChatGPT二、ChatGPT的作用三、免费ChatGPT的使用四、写在最后 一、什么是ChatGPT ChatGPT全称为Chat Generative Pre-trained Transformer,Chat是聊天的意思,GPT是生成型预训练变换模型,可以翻译为聊天生成预训练转换器或…

抖音seo源码开发部署

抖音seo账号矩阵源码系统搭建,​ 抖音获客系统,抖音SEO优化系统源码开发,思路分享,分享一些开发的思路...... 账号矩阵霸屏系统源代码账号矩阵系统建设部署,短视频seo账号矩阵框架分析,开发语言为后台框架语言PHP pyt…

chatgpt赋能Python-python_nonetype报错

Python NoneType报错:原因、解决方法和预防措施 Python 是一种面向对象的高级编程语言,用于快速编写脚本和应用程序。但是,当我们在编写 Python 代码时,可能会遇到 NoneType 报错;这是一种类型错误,它发生…

接口自动化测试工具SoapUI下载安装以及简单使用教程

前言 SoapUI是Webservice开发的必备工具。SoapUI是一个开源测试工具,通过Soap/HTTP来检查、调用、实现Web Service的功能,而且还能对Webservice做性能方面的测试。SoapUI会根据WSDL的格式生成左边的列表树,双击Request1就能看到Soap请求报文的内容。 一…