【JavaEE初阶】深入理解多线程阻塞队列的原理,如何实现生产者-消费者模型,以及服务器崩掉原因!!!

news2024/12/22 2:12:16

前言:

🌈上期博客:【JavaEE初阶】深入解析单例模式中的饿汉模式,懒汉模式的实现以及线程安全问题-CSDN博客

🔥感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

⭐️小编会在后端开发的学习中不断更新~~~  

🥳非常感谢你的支持

目录

📚️1.阻塞队列

 1.1阻塞队列的特性

1.2阻塞队列的使用

1.实例化

 2.入队列

3.出队列

1.3阻塞队列的实现

1.构造环形队列,实现出入队列功能

2.在多线程中解决线程安全问题

3.多线程实现阻塞唤醒操作

 📚️2.生产者-消费者模型

2.1生产者消费者模型的模拟 

2.2生产者消费者模型的作用

1.解耦合

2.削峰填谷

2.3生产者消费者模型代价

2.4生产者消费者模型模拟

📚️3总结


📚️1.阻塞队列

 1.1阻塞队列的特性

所谓的阻塞队列即对于普通队列做出的扩展,这个队列具有普通队列的特性,先进先出,并且阻塞队列是线程安全的,具有阻塞的功能~~~

阻塞功能:

如果针对一个已经满了的阻塞队列,在往队列里加入数据,此时队列就会发生阻塞,直到阻塞到队列不为满为止~~~

如果针对一个已经空了的阻塞队列,在往队列里取出数据,此时队列就会发生阻塞,直到阻塞到队列不为空为止~~~

1.2阻塞队列的使用

1.实例化

在Java标准库中有阻塞队列的数据结构,如下代码所示:

 BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(4);

 这里的ArrayBlockingQueue<>即用数组实现的阻塞队列,里面的4就是存储量

这里还存在用链表实现,和优先级队列实现的优先级队列,如下代码所示:

 BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(4);
 BlockingQueue<Integer> queue1=new LinkedBlockingQueue<>(4);
 BlockingQueue<Integer> queue2=new PriorityBlockingQueue<>(4);
 2.入队列

这里使用put或者offer来进行入队列,但是这里的put具有阻塞的功能,而offer没有,代码如下:

queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4);

这里再添加数据,我们看debug一下,测试:

此时我们能够看到使用put在达到存储最大值时,就不再添加数据了;

使用offer添加数据时,我们再次Debug一下后:

queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.offer(5));

此时我们打印一个false,且结束进程,所以offer是不会发生阻塞的,当队列满了的时候,插入数据成功会返回TRUE反之返回一个FALSE;

3.出队列

 这里就是使用take来进行出队列,且这里的take也具有阻塞的功能,代码如下:

queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.take());
System.out.println(queue.take());

当阻塞队列为空的时候,出队列会发生阻塞,即进程无法结束~~~ 

1.3阻塞队列的实现

 这里小编将使用数组来进行阻塞队列的实现,这里的队列就是环形队列;

1.构造环形队列,实现出入队列功能

代码如下:

class Myblockingqueue{
    //声明一个环形数组
    private int[] elems=null;
    private int head=0;
    private int tail=0;
    private int size=0;   
    //在实例化时,初始化这个数组
    public Myblockingqueue(int capcity){
        elems=new int[capcity];
    }
    public void put(int elem)  {                         
            if (size==elems.length){
                return ;
            }
            elems[tail]=elem;
            size++;
            tail++;
            if (tail>elems.length-1){
                tail=0;
            }                    
    }
    public int take() {
            int elem=0;             
            if (size==0){
                
               return 0;
            }
            elem=elems[head];
            head++;
            size--;
            if (head>elems.length-1){
                head=0;
            }           
            return elem;        
    }
}

这里对于环形队列的实现,小编就不再过多赘述了,前面数据结构有讲解

2.在多线程中解决线程安全问题

当多线程情况下,就会有以下问题,如图所示:

注意:这里当put最后一个数据的时候 线程1判断完条件后,别调度走了,然后此时线程2也满足条件,此时tail为100(假设存贮为100),然后线程1又调度回CPU执行剩下的,再次添加,那么就存在101个数据了,此时就发生了线程安全问题

解决:此时就需要加锁,使这串代码性质改变为原子性,使cpu调度其他线程发生阻塞

代码如下:

 synchronized (lock){
            //判断条件,是否满了,满了就进入阻塞
            if(size==elems.length){
                return;
            }
            elems[tail]=elem;
            size++;
            tail++;
            if (tail>elems.length-1){
                tail=0;
            }
         
        }

此时的出队列方法也是一样的,进行加锁,解决线程安全问题;

3.多线程实现阻塞唤醒操作

 在上述理解中,我们当队列为满的时候,入队列方法就要进行阻塞,那么唤醒时机就是出队列后;队列为空的时候,出队列就要进入阻塞,那么唤醒时机就是入队列之后;

入队列方法如下:

 synchronized (lock){
            //判断条件,是否满了,满了就进入阻塞
            if(size==elems.length){
                lock.wait();
            }
            elems[tail]=elem;
            size++;
            tail++;
            if (tail>elems.length-1){
                tail=0;
            }
            //唤醒取出线程
            lock.notify();
        }

 同理出队列方法如下:

        int elem=0;
        //判断条件,如果为空,那么就进入阻塞
        synchronized (lock){
            if (size==0){
                //进入线程等待
                lock.wait();
            }
            elem=elems[head];
            head++;
            size--;
            if (head>elems.length-1){
                head=0;
            }
            lock.notify();
            return elem;
        }

此时就解决了阻塞和唤醒线程的操作,但是此时任然有问题~~~

注意:当有两入队列线程,此时出队列后唤醒一个入队列线程后,思路是添加数据后满了需要唤醒出队列操作,但是由于锁对象是一样的,可能唤醒的就是另一个线程的入队列操作~~~

图示:

 

由于if只是判断一次,由于多线程的原因,肯会有很多变数,所以在被唤醒后,还需要判断是否满足队列满的条件,那么解决办法即将if改为while

代码如下:

public void put(int elem) throws InterruptedException {
        //防止多线程状态下,发生线程安全问题
        synchronized (lock){
            //判断条件,是否满了,满了就进入阻塞
            while (size==elems.length){
                lock.wait();
            }
            elems[tail]=elem;
            size++;
            tail++;
            if (tail>elems.length-1){
                tail=0;
            }
            //唤醒取出线程
            lock.notify();
        }
    }

 这里就是入队列的操作,那么同理出队列也是一样的,需要就那些while的循环判断~~~,那么此时我们的阻塞队列的实现就完成了;

 📚️2.生产者-消费者模型

注意:生产者消费者模型是根据阻塞队列来进行实现的 

2.1生产者消费者模型的模拟 

这里小编可以使用一个包饺子的过程来为大家展示这个模型的现实场景

在包饺子中,我们一般是一个人擀面,然后另外两个(假设)人进行包饺子,图示如下:

此时这里两个包饺子的人就是消费者线程,而擀面的人就是一个生产者线程;

此时的桌子即是一个阻塞队列,负责接收面皮,并传递面皮给包饺子的人;

注意:

假设擀面的人,擀饺子皮很快,那么此时就是生产大于消费,那么很快桌子就会被饺子填满,此时擀面的人就不能擀面了(类似于阻塞),然后两个包饺子的人包一个,擀面的就生产一个 面皮;

假设包饺子的人,包得很快,那么此时的消费就大于生产,那么很快桌子就为空了,那么包饺子的人就得等擀面的人(类似于阻塞),然后就是擀面的人擀出来一张面皮,包饺子的人就消费一张;

2.2生产者消费者模型的作用

1.解耦合

解耦合:将代码的耦合程度又高降低叫做解耦合~~~

在实际开发中,通常是“分布式系统”,即整个功能不是由一台服务器来完成的,而是每个服务器负责一部分功能,然后通过网络通信来进行连接,共同完成的;

那么当我们不使用阻塞队列时:

问题:此时由于没阻塞队列,A和B的耦合性就很强,A和C的耦合性就很强,A中的代码设计到B的一些相关操作,B也设计到A的一些相关操作,此时如要更改B,那么A也会受到影响,同理A和C也是一样的;

 那么加入阻塞队列:

注意:此时阻塞队列就是一个中介,服务器A不知道服务器B和C,此时A只需要传递数据给阻塞队列,再由阻塞队列传递给B和C,此时相应的耦合性就打打降低了;之间的影响也下降了;

2.削峰填谷

由于请求是客户端一方的需求,所以存在一时间突然请求骤增,那么此时就存在服务器崩掉的问题

就这个图来说:由于服务器A的功能简单,就传递数据给对应的功能服务器,那么它的抗压能力就更强,而BC服务器的功能复杂,需要消耗更多的资源,所以抗压能力更差; 

服务器挂掉原因:

由于服务器要处理每个请求,所以需要消耗硬件资源,这里包括(CPU,硬盘,内存,和网络带宽),此时其中一个硬件资源达到瓶颈,那么此时就不会对请求做出响应了,就导致服务器挂掉了

当我们引入阻塞队列时:

此时服务器A的数据传递给阻塞队列,由于阻塞队列只是用于存贮数据,所以抗压能力更强,可以控制请求传给功能服务器的速度,防止服务器B和C被大量的请求冲击而垮掉~~~

2.3生产者消费者模型代价

1.上述描述的阻塞队列,是以这个数据结构来实现的的服务器结构,会部署到单独的主机上

2.整个系统的结构变得复杂,需要维护的服务器就更多,成本变高了;

3.通过阻塞队列,实现A到B的数据传输的转化,需要一定的开销,影响效率

2.4生产者消费者模型模拟

此时我们就要用一个线程调用阻塞队列的入队列模仿生产者,一个线程调用出队列模仿消费者

代码如下:

 public static void main(String[] args) {
        Myblockingqueue queue=new Myblockingqueue(100);      
        //进行两个线程的测试
        Thread t1=new Thread(()->{
            int n=1;
            while (true){
                try {
                    queue.put(n);
                    System.out.println("生产的元素"+n);
                    n++;//当太多了就进入阻塞
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        Thread t2=new Thread(()->{
            while (true){
                try {
                    Thread.sleep(500);
                    int n=queue.take();
                    System.out.println("消费的元素"+n);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }

解释:此时代码由于消费者的消费进行了休眠操作,那么输出就是现生产大量的元素,然后消费一个生产一个;

如图:

这里小编只显示了一部分输出;

如果将休眠放在生产部分,那么代码如下:

 Thread t1=new Thread(()->{
            int n=1;
            while (true){
                try {
                    queue.put(n);
                    System.out.println("生产的元素"+n);
                    n++;//当太多了就进入阻塞
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });

那么输出就是:

消费一个,生产一个~~~

📚️3总结

💬💬小编本期讲解了关于阻塞队列的特性,实现过程中存在的问题,以及解决和代码的实现,并且还利用了阻塞队列实现了生产者消费者模型;并且还理解了生产者消费者模型在实际开发中作用

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!! 


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                             😊😊  期待你的关注~~~

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

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

相关文章

【在Linux世界中追寻伟大的One Piece】System V共享内存

目录 1 -> System V共享内存 1.1 -> 共享内存数据结构 1.2 -> 共享内存函数 1.2.1 -> shmget函数 1.2.2 -> shmot函数 1.2.3 -> shmdt函数 1.2.4 -> shmctl函数 1.3 -> 实例代码 2 -> System V消息队列 3 -> System V信号量 1 -> Sy…

K8S部署流程

一、war打包镜像(survey,analytics,trac系统) 代码打包成war准备tomcat的server.xml文件&#xff0c;修改connector中8080端口为项目的端口 修改前&#xff1a; <Connector port"8080" protocol"HTTP/1.1"connectionTimeout"20000"redirect…

idea环境下vue2升级vue3

在IDEA环境下&#xff0c;Vue2升级Vue3是一个非常重要的主题。在本文中&#xff0c;我们将介绍Vue2和Vue3之间的主要区别&#xff0c;以及如何在IDEA中升级Vue2项目到Vue3。我们还将讨论Vue3的新特性&#xff0c;如Composition API和Teleport等&#xff0c;并提供一些实用的代码…

快速掌握-vue3

是什么 vue2 的升级版&#xff0c; 使用 ts 重构了代码&#xff0c; 带来了 Composition API RFC。 类似于 react hook 的写法。 ts 重构&#xff0c;代码可读性更强vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty实现了 TreeShaking (当 Javascript 项目达到一定…

自闭症寄宿学校:为孩子发掘多重才能

在教育的广阔天地里&#xff0c;每一片土壤都孕育着不同的生命&#xff0c;每一颗种子都蕴含着无限的可能。对于自闭症儿童而言&#xff0c;他们的世界或许更加独特与复杂&#xff0c;但同样充满了未被发掘的潜能与才华。在广州&#xff0c;星贝育园自闭症儿童寄宿制学校正以满…

计算机毕业设计 Java酷听音乐系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

师生健康信息管理:SpringBoot技术突破

第4章 系统设计 4.1 系统体系结构 师生健康信息管理系统的结构图4-1所示&#xff1a; 图4-1 系统结构 登录系统结构图&#xff0c;如图4-2所示&#xff1a; 图4-2 登录结构图 师生健康信息管理系统结构图&#xff0c;如图4-3所示。 图4-3 师生健康信息管理系统结构图 4.2…

【Linux】用虚拟机配置Ubuntu环境

目录 1.虚拟机安装Ubuntu系统 2.Ubuntu系统的网络配置 3.特别声明 首先我们先要下载VMware软件&#xff0c;大家自己去下啊&#xff01; 1.虚拟机安装Ubuntu系统 我们进去之后点击创建新的虚拟机&#xff0c;然后选择自定义 接着点下一步 再点下一步 进入这个界面之后&…

element-ui 通过按钮式触发日期选择器

element ui 写在前面1. 自定义的日期时间组件CustomDatePicker.vue2. 页面效果总结写在最后 写在前面 需求&#xff1a;elementui中日期时间选择器&#xff0c;目前只能通过点击input输入框触发日期选择器&#xff0c;我希望能通过其他方式触发日期选择器同时把input输入框去掉…

Spring的IOC和DI入门案例分析和实现

前言 IOC和DI是spring的核心之一&#xff0c;那我们为什么要使用spring技术呢&#xff1f;spring技术的优点在哪里&#xff1f; spring的特点&#xff1a; 简化开发&#xff0c;降低企业级开发的复杂性框架整合&#xff0c;高效整合其他技术&#xff0c;提高企业级应用的开发与…

【Python报错已解决】TypeError: ‘NoneType‘ object is not callable

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

【常读常悟】《大数据之路-阿里巴巴大数据实践》一书读书摘要

【常读常悟】《大数据之路-阿里巴巴大数据实践》一书读书摘要 1、背景2、目录结构3、数据加工链路4、章节摘要4.1 第2章 日志采集4.1.1 日志采集方案4.1.2 采集指标 4.2 第3章 数据同步4.2.1 数据的特点4.2.2 数据同步的三种方式4.2.3 数据同步的最佳实践 4.3 第4章 离线数据开…

LabVIEW自动生成NI-DAQmx代码

在现代数据采集和控制系统中&#xff0c;LabVIEW被广泛应用于各种工业和科研领域。其中&#xff0c;NI-DAQmx是一个强大的驱动程序&#xff0c;可以帮助用户高效地管理和配置数据采集任务。本文将介绍如何在LabVIEW中通过DAQ Assistant Express VI和任务常量自动生成NI-DAQmx代…

VBA字典与数组第十九讲:VBA中动态数组的定义及创建

《VBA数组与字典方案》教程&#xff08;10144533&#xff09;是我推出的第三套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;字典是VBA的精华&#xff0c;我要求学员必学。7.1.3.9教程和手册掌握后&#xff0c;可以解决大多数工作中遇到的实际问题。…

【论文笔记】Visual Instruction Tuning

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: Visual Instruction Tunin…

Linux线程(二)线程ID及创建线程详解

1.线程ID 就像每个进程都有一个进程 ID 一样&#xff0c;每个线程也有其对应的标识&#xff0c;称为线程 ID。进程 ID 在整个系统中是唯一的&#xff0c;但线程 ID 不同&#xff0c;线程 ID 只有在它所属的进程上下文中才有意义。 进程 ID 使用 pid_t 数据类型来表示&#xf…

【Linux进程间通信】Linux匿名管道详解:构建进程间通信的隐形桥梁

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;Linux “ 登神长阶 ” &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀Linux进程间通信 &#x1f4d2;1. 进程间通信介绍&#x1f4da;2. 什么是管道&#x1f4dc;3…

22.1 k8s不同role级别的服务发现

本节重点介绍 : 服务发现的应用3种采集的k8s服务发现role 容器基础资源指标 role :nodek8s服务组件指标 role :endpoint部署在pod中业务埋点指标 role :pod 服务发现的应用 所有组件将自身指标暴露在各自的服务端口上&#xff0c;prometheus通过pull过来拉取指标但是promet…

期权卖方如何选择铁矿石行权价?期权策略盈亏分析计算方式详解

截止9月30日收盘&#xff0c;铁矿石2411合约收盘价825元/吨。日线级别处于上涨趋势中 假设以825元为最新价&#xff0c;假设后市铁矿石期货价格会下跌&#xff0c;期权卖方应该如何选择行权&#xff1f; 卖出行权价800的看涨期权&#xff0c;期权报价37.9&#xff0c;一手权利…