【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 插件开发 |(四)来查收…

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 --> …

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

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

上位机图像处理和嵌入式模块部署(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…

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.宝塔面板新增站点…

Vue3 上手笔记

1. Vue3简介 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece(n 经历了:4800次提交、40个RFC、600次PR、300贡献者 官方发版地址:Release v3.0.0 One Piece vuejs/core 截止2023年10月,最…

网盘——数据库操作

关于网盘的数据库模块,主要有以下几个内容:定义数据库操作类、将数据库操作类定义成单例模式、数据库操作 数据库是在Qt里面,定义成操作类,专门用这个类产生对象,对数据库实现操作,那么我们在产生对象的时…

BMS设计中的短路保护和MOSFET选型(下)

二、MOSFET参数 1、电气参数 (1)VGS :加在栅源两极之间的最大电压,一般为:-20V-+20V。 VGS额定电压是栅源两极间可以施加的最大电压。设定该额定电压的主要目的是防止电压过高导致的栅氧化层损伤。实际栅氧化层可承受的电压远高于额定电压,但是会随制造工艺的不同而改变…

01-机器学习概述

机器学习的定义 机器学习是一门从数据中研究算法的科学学科。 机器学习直白来讲, 就是根据已有的数据,进行算法选择,并基于算法和数据 构建模型,最终对未来进行预测。 机器学习就是一个模拟人决策过程的一种程序结构。 机器学…

PWM实现电机的正反转和调速以及TIM定时器

pwm.c #include "pwm.h"/* PWM --- PA2 --TIM2_CH3 //将电机信号控制一根接GND,一根接在PA2(TIM2_CH3), 输出PWM控制电机快慢 TIM2挂在APB1 定时器频率:84MHZ*/ void Pwm_Init(void) {GPIO_InitTypeDef GPIO_InitStruct;TIM_TimeBaseInitT…

Django下载使用、文件介绍

【一】下载并使用 【1】下载框架 (1)注意事项 计算机名称不要出现中文python解释器版本不同可能会出现启动报错项目中所有的文件名称不要出现中文多个项目文件尽量不要嵌套,做到一项一夹 (2)下载 Django属于第三方模块&#…

STM32微控制器中,如何处理多个同时触发的中断请求?

在STM32微控制器中,处理多个同时触发的中断请求需要一个明确的中断优先级策略,以确保关键任务能够及时得到响应。STM32的中断控制器(NVIC)支持优先级分组,允许开发者为不同的中断设置抢占优先级和子优先级。本文将详细…