【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个,然后继续生产,消费一个生产一个这样的速度了。


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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

02-MySQL数据库的基本使用与密码设置

一、服务端口 3306端口和33060端口,是我们启动数据库后开启的监听端口; 3306端口:是我们MySQL服务的监听端口,用来连接数据库使用; 33060端口:MySQL-shell服务的端口,MySQL-shell是MySQL架构集群…

day3-QT

1>使用手动连接,将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中,在自定义的槽函数中调用关闭函。将登录按钮使用qt5版本的连接到自定义的槽函数中,在槽函数中判断ui界面上输入的账号是否为"admin",密码是…

DBA工作经验总结

目录 一、MySQL8.0创建一张规范的表 1.表、字段全采用小写 2.int类型不再加上最大显示宽度 3.每张表必须显式定义自增int类型的主键 4.建表时增加comment来描述字段和表的含义(防止以后忘记) 5.建议包含create_time和update_time字段 6.核心业务增…

FloodFill算法——力扣被围绕的区域

文章目录 题目解析算法解析代码解析 题目解析 被围绕的区域 我们来解读一下这个题目,这个题目的意思就是求出被X围绕的O有多少个,那么什么是被围绕呢?也就是没有出路并且连通的O不能到四条边上,这就算是被围绕了,可是…

oracle 19c RAC补丁升级

1.停止集群件备份家目录 ----两节点分别操作 cd /u01/app/19.3.0/grid/bin/ crsctl stop crstar -zcvf /u01/app.tar.gz /u01/app/u01/app/19.0.0/grid/bin/crsctl start crs2.两节点 GI、DB OPatch 替换(都得执行) ----# 表示 root 用户,$…

npm、nrm、nvm详解与应用

本文全面介绍了 npm、nrm 以及 nvm 这三个与 Node.js 开发密切相关的工具。首先,对 npm 进行了定义和功能解释,包括其在依赖管理、项目管理、脚本执行、版本控制和社区贡献等方面的作用。接着,详细介绍了 npm 的常用命令和设置下载源的操作&a…

SqlServer找不到SQL Server Configuration Manager(配置管理)

1、Win键 R ,输入 compmgmt.msc 2、找到Sql Server配置管理器

iOS开发 - 转源码 - __weak问题解决

iOS开发 - 转源码 - __weak问题解决 在使用clang转换OC为C代码时,可能会遇到以下问题 cannot create __weak reference in file using manual reference 原因 __weak弱引用是需要runtime支持的,如果我们还只是使用静态编译,是无法正常转换的…

PCIe总线-PCIe总线简介(一)

1.概述 早期的计算机使用PCI(Peripheral Component Interconnect)总线与外围设备相连,PCI总线使用单端并行信号进行数据传输,由于单端信号很容易被外部系统干扰,其总线频率很难进一步提高。目前,为了提高总…

文件夹读取不到文件:深度解析与高效恢复策略

一、遭遇文件夹读取难题:文件离奇失踪 在日常使用电脑或移动设备的过程中,我们有时会遇到一个令人头疼的问题:原本存储着重要数据的文件夹突然变得“空空如也”,其中的文件仿佛凭空消失一般,无法正常读取。这种文件夹…

开源博客项目Blog .NET Core源码学习(10:App.Framwork项目结构分析)

开源博客项目Blog的解决方案总共包括4个项目,其中App.Hosting项目包括所有的页面及控制器类,其它项目主要提供数据库访问、基础类型定义等。这四个项目的依赖关系如下图所示,本文主要分析App.Framwork项目的主要结构及主要文件的用途。   …

IDEA 远程调试

1.什么是远程调试 Java提供了一个远程调试功能,支持设置断点及线程级的调试同时,不同的JVM通过接口的协议联系,本地的Java文件在远程JVM建立联系和通信。 2.服务端开启远程调试 开启远程调试功能,需要修改tomcat 的catalina.sh…

Spring Cloud Gateway Server MVC

之前你如果要用spring cloud gateway ,就必须是webflux 的,也就是必须是异步响应式编程。不能和spring mvc 一起使用。现在spring cloud 新出了一个可以不用webflux的gateway。 具体使用mvc的gateway步骤如下 普通的Eureka Client的项目 如果你只是想测…

unity无法使用道路生成插件Road Architect(ctrl和shift无法标点)

切换一下布局就行了。 附:Road Architect教学地址

以行动激发消费活力,加多宝引领高品质消费浪潮

2024年“315”期间,加多宝携手全国多地市场监督管理局、消费者协会等单位,围绕今年“激发消费活力”主题,积极配合各地相关政府部门开展系列宣传活动,以实际行动呼吁切实保护消费者合法权益,共建诚信消费环境&#xff…

Kubernetes概念:服务、负载均衡和联网:2. Gateway API

Gateway API 官方文档:https://kubernetes.io/zh-cn/docs/concepts/services-networking/gateway/ Gateway API 通过使用可扩展的、角色导向的、 协议感知的配置机制来提供网络服务。它是一个附加组件, 包含可提供动态基础设施配置和高级流量路由的 API…

机器学习——AdaBoost算法

机器学习——AdaBoost算法 在机器学习领域,AdaBoost算法是一种非常流行的集成学习方法,旨在提高分类器的性能。本篇博客将介绍AdaBoost算法的原理、算法流程、算法参数,对比AdaBoost和随机森林的区别,并使用Python实现AdaBoost算…

数据结构·排序

1. 排序的概念及运用 1.1 排序的概念 排序:排序是将一组“无序”的记录序列,按照某个或某些关键字的大小,递增或递减归零调整为“有序”的记录序列的操作 稳定性:假定在待排序的记录序列中,存在多个具有相同关键字的记…

[Java基础揉碎]单例模式

目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…

Docker进阶:Docker-cpmpose 实现服务弹性伸缩

Docker进阶:Docker-cpmpose 实现服务弹性伸缩 一、Docker Compose基础概念1.1 Docker Compose简介1.2 Docker Compose文件结构 二、弹性伸缩的原理和实现步骤2.1 弹性伸缩原理2.2 实现步骤 三、技术实践案例3.1 场景描述3.2 配置Docker Compose文件3.3 使用 docker-…