多线程的典型例子——阻塞队列

文章目录

  • 一、什么是阻塞队列?
  • 二、阻塞队列的功能
    • 2.1 线程安全
    • 2.2 具有阻塞功能
  • 三、阻塞队列的作用(生产者——消费者模型的作用)
    • 3.1 生产者-消费者模型
    • 3.2 解耦合
    • 3.3 削锋填谷
    • 什么是消息队列
    • 什么是中间件?
  • 四、阻塞队列的具体使用
    • 4.1 使用标准库提供的类
    • 4.2 自我实现一个阻塞队列

一、什么是阻塞队列?

学习了数据结构,我们会接触到 队列 此数据结构。

队列的特点是 先进先出 。那么如何理解“先进先出”这个概念呢?

下图中,右边是一组数:0 1 2 3 4 5 6 7 8,从左至右依次将数字放入队列queue中,再依次将队列中的数字取出来,此时由于0先进队列,因此0也是先出队列的。

在这里插入图片描述
再举个例子。排队做核酸。先排队的人,就能够先做完核酸,后排队的人,就只能晚点做核酸。在这里插入图片描述

队列有许多种类,譬如 优先级队列、消息队列、阻塞队列。此处我们重点介绍 阻塞队列。阻塞队列具有队列的一切特点,也是具有先进先出的特点。但由于阻塞队列是队列的一种特例,因此阻塞队列有自己的功能特点。

二、阻塞队列的功能

2.1 线程安全

队列是线程不安全的,但阻塞队列是线程安全的。

2.2 具有阻塞功能

阻塞队列除了线程安全,还具有阻塞功能。其阻塞功能具体表现在:

1、元素入队列时,如果当前队列已经满了,就会队列阻塞,直到队列不满(队列有空闲大小时),该元素才能入队列成功。
在这里插入图片描述

2、元素出队列时,如果队列为空,队列就会阻塞,直到队列不空,元素才能出队。
在这里插入图片描述

三、阻塞队列的作用(生产者——消费者模型的作用)

阻塞队列的应用场景一般是在:生产者——消费者模型 中使用。

生产者——消费者问题,描述的是多线程协同工作的一种方式。 而阻塞队列又是生产者——消费者模型的一个应用场景。

3.1 生产者-消费者模型

       那我们怎么理解 生产者——消费者模型是描述多线程协同工作的一种方式 这句话呢??

       举个例子。教师节,A、B、C三个学生打算折1000只千纸鹤当作礼物送给老师,他们准备了很多种颜色的彩纸,因为只买了一把剪刀,所以A同学负责把彩纸剪切成小纸张,B同学、C同学就负责使用小纸张折成千纸鹤。

       如果A同学剪切彩纸的速度很快,B、C同学折叠千纸鹤的动作稍慢,导致桌子上剩余了很多彩纸,此时A同学就可以稍作休息,等待B、C同学把桌上剩余的小纸张消耗得差不多了,再开始继续剪切彩纸。

       相反的,如果B、C同学折叠千纸鹤的速度飞快,不到一会儿就将桌子上的小纸张消耗得一干二净,此时B、C就可以稍作休息,等待A同学剪切更多的彩纸放到桌子上之后,再继续折叠千纸鹤。

A、B、C同学3个人虽然各司其职,但是却又是为了1000只千纸鹤这同一个目标而做事(协同工作)。在协同工作的过程中,会遇到各种场景:桌子上的彩纸过多时(阻塞队列中资源过多,队列满了),A同学稍作休息(阻塞等待);桌子上的彩纸没有时(阻塞队列中没有资源,队列空了),B、C同学稍作休息(阻塞等待)。

在这里插入图片描述

3.2 解耦合

       系统中含有A、B两个服务器,A、B服务器之间进行通信。A服务器发送请求,B服务器响应后返回数据给A服务器。随着业务场景的变化,新增了C服务器,C服务器也希望收到来自A服务器的请求,此时A服务器就需要修改自身代码为C服务器提供接口,以保证C服务器也能收到请求。
在这里插入图片描述

       此时A服务器需要修改的代码还不复杂,但是如果随着业务复杂度的提高,有成百上千个服务器也希望A服务器能够为他们提供接口以收到来自A服务器的请求,此时,A服务器需要修改的代码量就很庞大,同时 A服务器与这些服务器之间的耦合过高,如果后续其他服务器出现了什么问题,直接影响到A服务器的使用,同样,如果A服务器出现了什么问题,也直接影响到其他服务器的使用。

在这里插入图片描述
       为了进行解耦合,在A服务器与其他服务器之间加入一个 阻塞队列。其他服务器通过这个阻塞队列与A服务器进行间接通信,如果还有其他服务器想要收到来自A服务器的请求,只需要修改阻塞队列这个中间件中的代码即可,此时无论哪一个服务器出了什么问题,都影响不到其他服务器,此时,服务器之间的耦合大大降低。
在这里插入图片描述

3.3 削锋填谷

       众所周知,河流是人类的发源地。人类一般以部落为群,居住在河的下流。假如说,有一天由于雨季频繁,上流发大水,湍急的水流冲刷到下流,此时居住在下流的人类就会遭遇洪灾。因此为了避免此类自然灾难,人类就想到了在河流的中部修建一座水库,当上流发大水时,湍急的大水先来到水库,水库储存这些湍急的水流,然后再缓慢的将这些湍急的水流排至下流,此时,该水库作为下流的一个缓冲,下流就能避免再遭受湍急的洪灾了,哪怕上流的水量巨大且湍急,水库也无法储存这么多水流,水库的存在,也可以为下流的疏散争取时间。如果遇上旱期,下流没有水流了,也可以打开水库,将水库中储存的水流排至下流,以供人类日常生活。

在这里插入图片描述
阻塞队列就是这么一个存在。

什么是消息队列

所谓的消息队列(Message Queue),就是将阻塞队列这样的数据结构,单独提取出来作为一个程序,部署在一组服务器上。

消息队列服务器,也就是我们平常所说的MQ,也是一种常见的“中间件”。

什么是中间件?

中间件是一类“通用的”服务器的统称。当我们进行书写代码的时候,一部分是业务代码,一部分是通用代码(与业务无关的代码)。那怎么理解“通用代码”这个词呢?

举个例子:像百度公司其涉及的业务一般是搜索、腾讯公司一般涉及的业务是游戏、社交,阿里公司一般涉及的业务是电商,他们的业务各不相同,因此开发时所写的业务代码也不同,但是他们都需要进行数据的存储,因此此时进行数据存储的代码就是通用代码。

因此就有人想出可以专门弄一些程序来负责数据存储,这个程序做好了之后,任何有数据存储需求的开发者、公司都可以使用该程序进行数据存储。

一般我们选择使用数据库进行数据的持久化存储,目前主流的数据库有MySql、redis、sql server…像数据库这样的程序就可以视为一个“中间件”。

目前主流的消息队列有RabbitMQ、RocketMq、kafka、ActiveMQ。

四、阻塞队列的具体使用

4.1 使用标准库提供的类

标准库为我们提供了一个类:BlockingQueue ,由于这是一个接口,不能直接进行 new 实例,因此我们可以基于链表/数组实现阻塞队列。
在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> blockingDeque
         = new LinkedBlockingDeque<>(100);
        // 往队列中插入3个元素
        blockingDeque.put(1);
        blockingDeque.put(2);
        blockingDeque.put(3);
        //将队列里的3个元素全取出来并打印在控制台上
        Integer ret1 = blockingDeque.take();
        System.out.println(ret1);

        Integer ret2 = blockingDeque.take();
        System.out.println(ret2);

        Integer ret3 = blockingDeque.take();
        System.out.println(ret3);
       //将队列里的3个元素全取出来了,此时队列为空
       //尝试在为空的队列中继续取元素,此时会发生什么?
        Integer ret4 = blockingDeque.take();
        System.out.println(ret4);
    }

我们已经知道了阻塞队列的特点了,所以在为空的阻塞队列里继续取元素,线程会阻塞等待,因此此时控制台并不会结束进程,光标一闪一闪的,表示此时被阻塞。

在这里插入图片描述
阻塞队列中,一般使用put() 方法进行入队列操作,offer() 方法也是用于入队列操作,但是一般不推荐使用该方法进行入队列操作。使用 take() 方法 进行出队列操作。

put()方法是往队列尾插入一个元素,take()方法是从队列首取出一个元素。
在这里插入图片描述

如果有想要深入了解 阻塞队列 的同学,可以对阻塞队列的源码进行一番研究,多专注、琢磨源码。可能有同学不知道这个阻塞队列的源码在哪里找,其实只要我们在IDEA中书写 阻塞队列 BlockingQueue这个接口后,将鼠标移动到BlockingQueue这个接口上,然后按住ctrl键不松开,同时鼠标点击BlockingQueue这个接口,IDEA会自动跳转到BlockingQueue这个接口这个源码里,此时我们就可以查看一些接口、类、方法的源码了。
在这里插入图片描述

4.2 自我实现一个阻塞队列

public class testBlockingQueue2 {
//    自定义数组大小
    private static final int CAPACITY_COUNT = 100;
//    基于数组实现队列
    private volatile int[] arr = new int[CAPACITY_COUNT];
//    指向队列头
    private volatile int head = 0;
//    指向队列尾
    private volatile int tail = 0;
//    队列中的有效元素个数
    private volatile int size = 0;

//    该方法用于实现插入元素至队列
    public void put(int elem){
        synchronized (this){
//            判断当前队列是否满
            if (size >= arr.length){
                try {
//                    队列满,则阻塞等待
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//            将元素插入队列尾
            arr[tail] = elem;
//            插入新元素后,指向队列尾的指针 + 1
            tail++;
//            判断指向队列尾部的指针是否大于或等于队列长度,大于则让tail指向队列首
            while (tail >= arr.length){
                tail = 0;
            }
//            队列插入新元素后,队列有效元素个数 + 1
            size++;

//            当插入元素后,队列不为空,此时可以take()取元素,因此此处使用notify()进行唤醒
            this.notify();
        }
    }



//    使用该方法取出队列中的元素(取的是队列的首元素)
    public Integer take(){
        synchronized (this){
//            判断队列是否为空,队列为空则阻塞等待
            while (size == 0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//            队列不为空,将队列首元素取出来
            int ret = arr[head];
//            指向队列首的指针 + 1
            head++;

//            判断 head 是否走到队列末尾或者超过队列长度,如果是,让 head 重新指向队列头部
            if (head >= arr.length){
                head = 0;
            }
//            将队列首元素取出队列成功后,队列有效元素个数 - 1
            size--;
//            队列取出元素后,说明队列此时不是满的,可以进行put()操作,使用notify()唤醒put()方法的阻塞
            this.notify();
//            将取出来的队列首元素返回
            return ret;
        }
    }

}



class test{
    public static void main(String[] args) {
        testBlockingQueue2 queue2 = new testBlockingQueue2();
//        通过匿名内部类来创建两个线程进行演示生产者-消费者模型
        Thread producer = new Thread(() -> {
            int n = 0;
            while (true){
                try {
                    queue2.put(n);
                    System.out.println("producer 生产元素 = " + n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true){
                try {
                    int n = queue2.take();
                    System.out.println("consumer 消费元素 = " + n);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

//        启动线程
        producer.start();
        consumer.start();
    }
}

自我实现一个阻塞队列,主要的难点是思考如何实现put()、take()的逻辑,顺利使用put()进行元素入队列,take()进行元素出队列操作。

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

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

相关文章

上拉电阻和上拉能力

大家好&#xff0c;我是记得诚。 关于上下拉电阻&#xff0c;之前写过一篇文章&#xff1a;聊一聊上拉电阻、下拉电阻、使用场景及阻值选择 有个做测试的读者&#xff0c;想转行硬件&#xff0c;之前一直在学习&#xff0c;也加入了我的硬件工程师小密圈 今天问了我一个问题…

Vue3全家桶 - VueRouter - 【6】导航守卫

导航守卫 查看以下情形&#xff1a; 点击主页链接时&#xff0c;默认情况下可直接进入指定页面&#xff0c;如下图&#xff0c;但是问题是该跳转的界面是需要用户登录后方可访问的&#xff1b; 可设置导航守卫来检测用户是否登录&#xff0c;如果已登录&#xff0c;则进入后台…

代码随想录day18(2)二叉树:翻转二叉树(leetcode226)

题目要求&#xff1a;将一棵二叉树翻转 思路&#xff1a;若想要翻转二叉树&#xff0c;只需要用swap函数将左右孩子节点翻转即可。注意前序和后序遍历均可&#xff0c;但是对于中序来说会将某些结点的左右孩子翻转了两次&#xff08;画图很明显&#xff09;&#xff0c;硬要用…

Yolov8-车辆跟踪(BoT-SORT和ByteTrack算法)

这两种代码都是成熟的&#xff0c;直接调佣即可&#xff0c;下面是使用这两种算法的代码。 直观感受&#xff1a;ByteTrack预测的速度感觉比BoT-SORT快一些&#xff0c;流畅一些。 from ultralytics import YOLOmodel YOLO(yolov8n.pt)# results model.track(source".…

快速实现主数据管理价值——敏捷型实施方法在某激光设备龙头企业项目中的应用

先前我们介绍了两种常用的主数据项目的实施方法——瀑布型实施方法和敏捷型实施方法。 本期&#xff0c;我们将为大家介绍敏捷型实施方法的成功案例&#xff1a;某激光设备龙头公司的主数据项目。 在介绍案例前&#xff0c;先回顾下上期的内容。 瀑布模型和敏捷模型是当下流行…

SpringCloud OpenFeign 服务接口调用

一、前言 接下来是开展一系列的 SpringCloud 的学习之旅&#xff0c;从传统的模块之间调用&#xff0c;一步步的升级为 SpringCloud 模块之间的调用&#xff0c;此篇文章为第四篇&#xff0c;即介绍 Feign 和 OpenFeign 服务接口调用。 二、概述 2.1 Feign 是什么 Feign 是一…

车载诊断协议DoIP系列 —— 地址解析协议(ARP)邻居发现协议(NDP)因特网控制消息协议(ICMP)

车载诊断协议DoIP系列 —— 地址解析协议(ARP)&邻居发现协议(NDP)&因特网控制消息协议(ICMP) 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输…

《JAVA与模式》之装饰模式

系列文章目录 文章目录 系列文章目录前言一、装饰模式的结构二、齐天大圣的例子三、装饰模式的简化四、装饰模式的优缺点五、设计模式在JAVA I/O库中的应用前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章…

ssm蛋糕甜品商城系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

程序人生——Java开发中通用的方法和准则,Java进阶知识汇总

目录 引出Java开发中通用的方法和准则建议1:不要在常量和变量中出现易混淆的字母建议2:莫让常量蜕变成变量建议3:三元操作符的类型务必一致建议4:避免带有变长参数的方法重载建议5:别让null值和空值威胁到变长方法建议6:覆写变长方法也循规蹈矩建议7:警惕自增的陷阱建议…

AI绘画怎么用?详细教程在这里!

AI绘画是一种利用人工智能技术来创作艺术作品的方式。以下是一个详细的AI绘画的详细教程&#xff0c;介绍AI绘画怎么用? 1. 选择合适的AI绘画工具&#xff1a;市面上有许多AI绘画工具供用户选择&#xff0c;如建e网AI、DeepArt、DALL-E等。用户可以根据自己的需求和兴趣&#…

弹出U盘时提示“该设备正在使用中”怎么办?

当我们在弹出U盘是遇到“该设备正在使用中”的提示时&#xff0c;若强行拔除U盘&#xff0c;可能会导致数据损坏或丢失&#xff0c;那么应该如何处理这种情况以使U盘安全弹出呢&#xff1f; 弹出U盘时提示“该设备正在使用中”的原因 弹出U盘时提示“该设备正在使用中”的原因…

【历年论文真题考点汇总】与【历年论文原题2009~2023年文字版记录】(2024年软考高级系统架构设计师冲刺知识点总结-论文篇-先导篇)

历年真题论文题考点汇总 历年软考系统架构设计师论文原题(2009-2022年) 因最新的2023年目前仅能搜索到回忆版,等楼主搜集到真题会更新最新版到本文中。 注意系统架构设计师一年只下半年开考,项目管理师一年两次开考。 2022年下半年-论文原题 试题1:论基于构件的软件开发…

【MAC】MacOS M2 芯片的Mysql 数据库安装与使用

1.下载 https://downloads.mysql.com/archives/community/ 选择ARM的 2.安装 在安装到最后一步&#xff1a;configuration 一定要选择Use Legacy Password Encryption。 一定要记得输入密码&#xff0c;这个密码也是登陆mysql的密码&#xff0c;非常重要。备注&#xff1a;…

SpringSecurity原理简述

文章目录 0. 简介1. 快速入门1.1 准备工作1.2 引入SpringSecurity 2. 认证2.1 登陆校验流程2.2 原理初探2.2.1 SpringSecurity完整流程2.2.2 认证流程详解 2.3 解决问题2.3.1 思路分析2.3.2 准备工作2.3.3 实现2.3.3.1 数据库校验用户准备工作核心代码实现 2.3.3.2 密码加密存储…

【QT】自定义控件的示例

自定义控件&#xff08;很重要&#xff09; 什么是自定义控件&#xff1f; 顾名思义就是创建一个窗口&#xff0c;放入多个控件&#xff0c;拼接起来&#xff0c;一起使用。 为什么需要它&#xff1f; 需求&#xff0c;假设有100个窗口&#xff0c;那如果有两个控件同时被使…

基于PPT战略的河南嵩县旅游扶贫模式研究

目录 摘 要 3 Abstract 3 &#xff08;一&#xff09;研究背景 4 &#xff08;二&#xff09;研究意义 5 &#xff08;三&#xff09;研究目的 6 二、概念界定及相关研究 6 &#xff08;一&#xff09;PPT战略 6 &#xff08;二&#xff09;PPT战略相关研究 6 &#xff08;三&…

JMeter 二次开发之环境准备

通过JMeter二次开发&#xff0c;可以充分发挥JMeter的潜力&#xff0c;定制化和扩展工具的能力以满足具体需求。无论是开发自定义插件、函数二次开发还是定制UI&#xff0c;深入学习和掌握JMeter的二次开发技术&#xff0c;将为接口功能测试/接口性能测试工作带来更多的便利和效…

19、deque赋值操作

#include <iostream> using namespace std; #include <deque>void printdeque (const deque<int>& d) {for (deque<int>::const_iterator it d.begin(); it ! d.end(); it ){//*it 100 容器中的数据不可修改cout << *it << " &…

Java剖析 : HashMap底层存储数据的结构 | HashSet添加不重复元素底层原理

HashSet底层剖析 前言&#xff1a; 我们知道Set中所存储的元素是不重复的&#xff0c;那么Set接口的实现类HashSet在添加元素时是怎么避免重复的呢&#xff1f; ★ HashSet在添加元素时&#xff0c;是如何判断元素重复的? ● 在底层会先调用hashCode()&#xff0c…