【JavaEE初阶系列】——阻塞队列

目录

🚩阻塞队列的定义

🚩生产者消费者模型

🎈解耦性

🎈削峰填谷 

🚩阻塞队列的实现

📝基础的环形队列

📝阻塞队列的形成

📝 内存可见性

📝阻塞队列代码


🚩阻塞队列的定义

阻塞队列是一种特殊的队列,也遵循“先进先出”的原则。

阻塞队列能是一种线程安全的数据结构 , 并且具有以下特性 :
  • 1.线程安全
  • 2.带有阻塞特性

    a) 如果队列为空,继续出队列,就会发生阻塞。阻塞到其他线程往队列里添加元素为止。

    b)如果队列为满,继续入队列,就会发生阻塞。阻塞到其他线程往队列里添加元素为止。

阻塞队列,最大的意义,就是可以用来实现“生产者消费者模型”——一种常见的,多线程代码编写方式。


🚩生产者消费者模型

🎈解耦性

生产者消费者模式就是通过一个容器来 解决生产者和消费者的强耦合 问题。
耦合:俩个模块,联系越紧密,耦合越高!

就比如就一个擀饺子皮杖,所以我们分配一个人擀饺子,剩下三个人包饺子。擀饺子皮就会不停的产出饺子皮,三个人负责包饺子,那三个人就不停的消耗饺子皮。我生产出来的饺子皮得有地方放,那就是放在板子上,而所谓的板子就是阻塞对列,进行来放入元素和放出元素。

生产者:把生产出来的内容,放到阻塞队列中

消费者:就会从阻塞队列中获取内容

如果我生产的慢,那么三个人就得等,就相当于从空的队列中获取元素就会阻塞

如果我生产的快,我就得等了,就相当于从满的队列中放入元素就会阻塞。


为什么要使用生产者消费者模型呢?给我们带来了什么好处呢?

解耦性:两个模块,联系越紧密,耦合就越高,尤其是分布式系统。

比如,考虑一个简单的分布式系统

如果A和B直接交互(A把请求发给B,B把响应返回到A)

彼此之间的耦合就是比较高的

  • 1)如果B出现问题,很可能就把A也影响到了
  • 2)如果未来再添加一个C,就需要对A这边的代码,做出一定的改动。

所以解决上述问题,使用生产者消费者模型,就可以有效的解决刚才的耦合问题。

 阻塞队列(当把阻塞队列封装成单独的 服务器程序部署到特定的机器上,这个时候就把这个队列,称为消息队列),此时的耦合度就会降低,如果B这边出现问题,就不会对A产生直接影响(A只是和对列交互,不知道B的存在)后续增加一个C,此时A不必进行任何修改,只需要C从队列中获取数据即可。


🎈削峰填谷 

短时间内,(削峰)请求量比较多,(填谷)请求量比较少。

这个结构下,一旦客户端这边发起的请求非常多了,每个A收到的请求,都会立即发给B,A这边抗多少访问量B,B和A完全一样。但是在不同的服务器下,上面跑的业务不同,虽然访问量一样,单个访问,消耗的硬件资源不一样,可能A承担这些并发量就会挂了~~。比如B要操作数据库,数据库本身就是一个分布式系统,相对脆弱的环节。

引入生产者消费者模型,上述问题也会得到很大的改善。

A这边收到较大的请求量,A会把对应的请求写入队列中,B仍然可以按照之前的节奏,来处理请求。 比如,正常情况下,A和B每秒处理1k请求,极端情况下,A这边每秒处理3K请求,如果让B也处理3k次,就要挂了。队列B承担了压力,B仍然可以按照1K次的节奏,处理请求。但是像上述的情况下不会一直持续的存在,只会短时间出现,过了峰值之后,A的请求就恢复正常了,B就可以逐渐的把积压的数据都给处理掉了。这就保证了整个系统在突发情况下,都能更好的控制。

就比如大坝中,如果上游水量增加,大坝关闸蓄水,把上游的压力分担很多,往下游去放水的时候,就有节奏的放,如果上游水量减少了,大坝开闸放水。


🚩阻塞队列的实现

在java标准库里,已经提供了线程的阻塞队列,让咱们直接使用。

标准库中,针对BlockingQueue提供了俩种重要的实现方式

  • 1.基于数组
  • 2.基于链表

 了解标准库的阻塞队列怎么用,固然是一个环节,更重要的,是我们能够自己实现一个阻塞队列

阻塞队列包含三部分

  • 基于一个普通的队列
  • 线程安全——加锁(保证原子性)
  • 阻塞 (队列空出元素,队列满入元素都会造成阻塞)
  • 内存可见性 volatile关键字

普通队列,可以基于数组,也可以基于链表,基于数组其实是环形队列。

head指向的是队列的头部位置,tail是每插入一个元素tail都往后移一位,[head,tail)构成了一个区间,这个区间里的内容就是当前队列中的有效元素~,入队列,把新的元素,放到tail位置上,同时tail++,出队列,把head指向的元素给删除掉,head++;

但是初始情况下,队列为空的时候 head和tail重合,队列满了情况下,head和tail又重合了。

解决方案:

1.浪费一个格子,让tail指向head的前一个位置,就算满了。

2.专门搞一个变量size,来表示元素的个数,size为0是空,为数组最大值,就是满。(本次基于这种方法来写)

BlockingQueue阻塞队列中有俩个方法:

  • put阻塞式的入队列
  • tale阻塞式的出队列

📝基础的环形队列

首先定义三个变量,对头,队尾,有效元素的个数。
  • 入队列的时候,如果队列满了,普通队列就直接返回了,不满就插入元素,如果tail等于数组的最大长度了,那么就让tail设置成0.
  • 如果队列为空,普通队列就直接返回了,不为空,就删除元素,如果head等于数组的最大长度,那么就让head=0即可
class MyBlockQueue{//此处的最大长度,也可以指定构造方法,由构造方法来设定private String[] data=new String[1000];//队列的起始位置private int head=0;//队列的结束位置的下一个位置private int tail=0;//队列中有效元素的个数private int size=0;//核心放大,入队列和出队列public  void put(String elem){if(size==data.length){//队列满了//如果普通队列就直接return了return;}//队列没满,真正的往里面添加元素data[tail]=elem;tail++;//如果tail自增之后,到达了数组的末尾,这个时候就需要让它回到开头(环形队列)if(tail== data.length){tail=0;}size++;}public String take(){if(size==0){return null;}//队列不为空,就队首元素就返回去,并且删除掉String ret=data[head];head++;if(head==data.length){head=0;}size--;return ret;}
}

 这很明显是线程不安全的情况,如果在多线程的情况,对一个变量修改,那是非常的不安全的。


📝阻塞队列的形成

所以我们首先需要加锁。

我们直接将方法内部所有的代码段都加锁,因为里面涉及到多个变量修改,所以就要加锁来保证原子性和线程安全。

如何进行阻塞呢?

  • a.如果队列为空,继续出队列,就会发生阻塞。阻塞到其他线程往队列里添加元素为止。
  • b.如果队列为满,继续入队列,就会发生阻塞。阻塞到其他线程往队列里添加元素为止。

所以这里需要wait和notify机制

一个队列,要么是空,要么是满。

take和put只有一边能阻塞。

  • 如果put阻塞了,其他线程继续调用put也都会阻塞,只有靠take唤醒
  • 如果take阻塞了,其他线程继续调用take也还是会阻塞,只有靠put唤醒。

当put方法,因为队列满了,进入wait之后,此时,wait返回(被唤醒的时候)队列一定是不满的嘛?wait除了notify之外,是否还有其他的方式唤醒呢?

interrupt是可以中断wait状态的,但是在用try catch捕捉异常的时候,如果没有throw抛出异常的话,代码会往下一直执行,那么下面的tail指向的元素给覆盖掉了,实际上此处队列是满着的,此时tail指向的元素,并非是无效元素(把一个有效元素给覆盖住了)

所以使用wait的时候,一定要注意,考虑当前wait唤醒,是通过notify唤醒,还是通过interrupt唤醒。

  • notify唤醒说明其他线程调用了take,此时队列已经不满了,可以继续添加元素
  • interrupt唤醒,此时队列还是满的,继续添加元素,肯定是会出现问题的。因为interrupt确实是可以让wait唤醒,1>如果try catch的捕获异常了后没有抛出异常的话,就会继续往下执行,2>如果try catch的捕获异常了后并且抛出异常,会终止了整个线程,代码没有什么问题。但是我们不能保证我们有没有抛出异常。

所以基于上述俩个情况,如果用notify唤醒的话,可以不用担心,因为唤醒了wait肯定是因为 调用了take删除元素才会唤醒的,但是如果用 interrupt唤醒之后,我们就要担心一下是否队列依旧是满的,因为interrupt调用之后,如果继续插入元素的话,就会出现问题。

关键要点,当wait返回的时候,需要进一步确认一下,看当前队列是不是满的(本来是因为队列满,进入阻塞,接触阻塞之后,再确定一下队列满不满,如果经过确定之后,队列还是满的,继续进行wait)

使用wait的时候,往往都是使用while作为条件判定的方式,目的就是为了让wait唤醒之后还能再确认一次,是否条件仍然满足。

对于interrupt的情况下,这样用while作为条件判定的方式,就会每次唤醒之后,还会继续判定一次,如果还是等于,就继续,这样就避免了有没有捕捉异常后是否有抛出异常了。

所以针对俩种情况,我们最好的情况下,都是用while来判定一下,防止出现bug现象。

本题主要针对的是wait() notify()情况下,也不排除用interrupt唤醒程序。


📝 内存可见性

内存可见性是指当我们对同一个资源进行多次大量的使用的时候,那么jvm就不会对这个资源的改变加载到主内存中去,而是加载到运行内存中,但是只有对一个值的改变加载到主内存才是真正得对这个资源得修改,内存可见性其实就是为了保证我们每一次对某个资源的修改都能加载到主内存中去,而不是因为这个资源可能在某个时间段内被多次使用,就被jvm选择,暂时不把它加载到主内存中去。 这样就会导致其他线程再读取当前变量值的时候,就会是原来的值。

所以我们对于阻塞队列中,我们如果消费者和生产者每次都生产的很多或者消费的很多的话,那么可能会被jvm直接加载到运行内存中去,那么就会导致bug存在。所以避免内存可见性情况,我们用volatile来修饰变量。


📝阻塞队列代码

package BlockingQueue;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;class MyBlockQueue {//此处的最大长度,也可以指定构造方法,由构造方法来设定private String[] data = new String[1000];//队列的起始位置private volatile int head = 0;//队列的结束位置的下一个位置private volatile int tail = 0;//队列中有效元素的个数private volatile int size = 0;//核心放大,入队列和出队列public void put(String elem) throws InterruptedException {synchronized (this) {while (size == data.length) {this.wait();}//队列没满,真正的往里面添加元素data[tail] = elem;tail++;//如果tail自增之后,到达了数组的末尾,这个时候就需要让它回到开头(环形队列)if (tail == data.length) {tail = 0;}size++;this.notify();}}public String take() throws InterruptedException {synchronized (this) {while(size == 0) {this.wait();}//队列不为空,就队首元素就返回去,并且删除掉String ret = data[head];head++;if (head == data.length) {head = 0;}size--;this.notify();return ret;}}
}
public class Test {public static void main(String[] args) {MyBlockQueue queue=new MyBlockQueue();//消费者Thread t1=new Thread(()->{while (true){try {String result=queue.take();System.out.println("消费元素"+result);//Thread.sleep(500);//生产慢 消费快} catch (InterruptedException e) {throw new RuntimeException(e);}}});//生产者Thread t2=new Thread(()->{int num=1;while (true){try {queue.put(num+" ");System.out.println("生产元素"+num);num++;Thread.sleep(500);//生产快,消费慢} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

我设定的是生产的慢,消费的快,每0.5s生产一个,我们可以看到,生产一个消费一个。 

 如果设定的生产的快,消费的慢,那么此时我们设定的数组长度是1000,等一下功夫都生产到1000了,然后等消费1个,然后继续生产,消费一个生产一个这样的速度了。


要努力成为自己想要的样子~

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

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

相关文章

【深度学习】pytorch,MNIST手写数字分类

efficientnet_b0的迁移学习 import torch import torch.nn as nn import torch.optim as optim import torchvision.transforms as transforms from torchvision.datasets import MNIST from torch.utils.data import DataLoader from torchvision import models import matplo…

IntelliJ IDE 插件开发 | (七)PSI 入门及实战(实现 MyBatis 插件的跳转功能)

系列文章 IntelliJ IDE 插件开发 |(一)快速入门IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化IntelliJ IDE 插件开发 |(三)消息通知与事件监听IntelliJ IDE 插件开发 |(四)来查收…

【mybatis】TypeHandler解读

在谈论MyBatis的源码时,TypeHandler 是其中一个非常关键的组成部分,它负责Java类型和JDBC类型之间的相互转换。理解TypeHandler的工作原理,对于深入理解MyBatis的数据处理流程十分重要。 什么是TypeHandler? 在MyBatis中,TypeH…

android Fragment 生命周期 方法调用顺序

文章目录 Introlog 及结论代码 Intro 界面设计:点击左侧按钮,会将右侧 青色的RightFragment 替换成 黄色的AnotherRightFragment,而这两个 Fragment 的生命周期方法都会打印日志。 所以只要看执行结果中的日志,就可以知道 Fragme…

【单例测试】Mockito实战

目录 一、项目介绍二、业务代码2.1 导入依赖2.2 entity2.3 Dao2.4 业务代码 三、单元测试3.1 生成Test方法3.2 引入测试类3. 3 测试前准备3.4 测试3.4.1 name和phone参数校验3.4.2 测试数据库访问 3.4.3 数据库反例 总结 前面我们提到了《【单元测试】一文读懂java单元测试》 简…

IDEA Android新建项目基础

title: IDEA Android基础开发 search: 2024-03-16 tags: “#JavaAndroid开发” 一、构建基本项目 在使用 IDEA 进行基础的Android 开发时,我们可以通过IDEA自带的新建项目功能进行Android应用开发基础架构的搭建,可以直接找到 File --> New --> …

vue的history路由实现形式

vue的路由实现形式 SPA single page web application,单页Web应用 简单的说SPA就是一个WEB项目只有一个HTML页面,一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载和跳转。取而代之的是利用JS动态的改变HTML的内容&#xff0c…

代码随想录算法训练营day19 | 二叉树阶段性总结

各个部分题目的代码题解都在我往日的二叉树的博客中。 (day14到day22) 目录 二叉树理论基础二叉树的遍历方式深度优先遍历广度优先遍历 求二叉树的属性二叉树的修改与制造求二叉搜索树的属性二叉树公共最先问题二叉搜索树的修改与构造总结 二叉树理论基础 二叉树的理论基础参…

基于nodejs+vue学生作业管理系统python-flask-django-php

他们不仅希望页面简单大方,还希望操作方便,可以快速锁定他们需要的线上管理方式。基于这种情况,我们需要这样一个界面简单大方、功能齐全的系统来解决用户问题,满足用户需求。 课题主要分为三大模块:即管理员模块和学生…

平台介绍-搭建赛事运营平台(1)

平台的一个很重要的市场方向是为企业搭建各类运营平台。运营平台是这类企业的核心系统,例如对银行而言就是柜台系统,对于电商而言就是电子商城。运营平台和内部信息平台的显著区别是要面向外部C端客户。内部信息平台的受众只是企业内部人员。 最近签约开…

HAL STM32G4 +ADC手动触发采集+各种滤波算法实现

HAL STM32G4 ADC手动触发采集各种滤波算法实现 📍相关篇《HAL STM32G4 TIM1 3路PWM互补输出VOFA波形演示》 ✨本篇内容也是继欧拉电子相关无刷电机驱动控制学习的相关基础内容。仅作为个人笔记记录使用。 📍感谢网友提供的相关内容《基于STM32的ADC采样及…

上位机图像处理和嵌入式模块部署(qmacvisual轮廓查找)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 前面我们说过,图像的处理流程一般都是这样的,即灰度化-》降噪-》边缘检测-》二值化-》开闭运算-》轮廓检测。虽然前面的几个…

LeetCode 面试经典150题 14.最长公共前缀

题目: 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符串 ""。 思路: 代码: class Solution {public String longestCommonPrefix(String[] strs) {if (strs.length 0) {return &…

知攻善防应急靶场-Linux(2)

前言: 堕落了三个月,现在因为被找实习而困扰,着实自己能力不足,从今天开始 每天沉淀一点点 ,准备秋招 加油 注意: 本文章参考qax的网络安全应急响应和知攻善防实验室靶场,记录自己的学习过程&am…

python绘图matplotlib——使用记录1

本博文来自于网络收集,如有侵权请联系删除 使用matplotlib绘图 1 常用函数汇总1.1 plot1.2 legend1.3 scatter1.4 xlim1.5 xlabel1.6 grid1.7 axhline1.7 axvspan1.8 annotate1.9 text1.10 title 2 常见图形绘制2.1 bar——柱状图2.2 barh——条形图2.3 hist——直…

flutter3_douyin:基于flutter3+dart3短视频直播实例|Flutter3.x仿抖音

flutter3-dylive 跨平台仿抖音短视频直播app实战项目。 全新原创基于flutter3.19.2dart3.3.0getx等技术开发仿抖音app实战项目。实现了类似抖音整屏丝滑式上下滑动视频、左右滑动切换页面模块,直播间进场/礼物动效,聊天等模块。 运用技术 编辑器&#x…

git标签的简单操作

创建标签 git tag v1.0 # 对head指向的commit创建标签 git tag v1.1 commit_id # 对指定的commit创建标签 git tag v2.0 -a -m "标签注释" commit_id # 创建注释标签查看标签 git tag -l v1* # 查看标签,匹配v1开头的 git show v2.0 # 查看标签详细信息…

Qt如何重写closeEvent

在 Qt 中,重写 closeEvent 函数是处理窗口关闭事件的一种方式。当你关闭一个 Qt 窗口时,该窗口会接收到一个 QCloseEvent 对象。通过重写窗口类的 closeEvent 函数,你可以自定义窗口关闭时的行为。 下面是一个简单的例子,展示了如…

Netty剖析 - Why Netty

文章目录 Why NettyI/O 请求的两个阶段I/O 模型Netty 如何实现自己的 I/O 模型线程模型 - 事件分发器(Event Dispather)弥补 Java NIO 的缺陷更低的资源消耗网络框架的选型Netty 发展现状Netty 的使用 Why Netty I/O 模型、线程模型和事件处理机制优化&a…

php搭建websocket

1.项目终端执行命令:composer require topthink/think-worker 2.0.x 2.config多出三个配置文件: 3.当使用php think worker:gateway命令时,提示不支持Windows。 4.打包项目为zip格式 5.打包数据库 6.阿里云创建记录 7.宝塔面板新增站点…