【多线程-从零开始-捌】阻塞队列,消费者生产者模型

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 【多线程-从零开始-捌】阻塞队列,消费者生产者模型

什么是阻塞队列

阻塞队里是在普通的队列(先进先出队列)基础上,做出了扩充

  1. 线程安全
  • 标准库中原有的队列 Queue 和其子类,默认都是线程不安全的
  1. 具有阻塞特性
  • 如果队列为空,进行队列操作,此时就会出现阻塞。一直阻塞到其他线程往队列里添加元素为止
  • 如果队列满了,进行队列操作,此时就会出现阻塞。一直阻塞到其他线程从队列里取走元素为止

基于阻塞队列,最大的应用场景,就是实现“生产者消费者模型”(日常开发中,常见的编程手法)

生产者消费者模型

比如:

小猪佩奇一家准备包饺子,成员有佩奇,猪爸爸和猪妈妈,外加一个桌子

  • 佩奇负责擀面皮
  • 猪爸爸和猪妈妈负责包饺子
  • 桌子用来放你擀好的面皮
    每次佩奇擀好一个面皮后,就放在桌子上,猪爸爸和猪妈妈就用这个面皮包出一个饺子

此时:

  • 佩奇就是面皮的生产者——生产者
  • 猪爸爸和猪妈妈就是面皮的消费者——消费者
  • 桌子就是阻塞队列——阻塞队列

为什么是是阻塞队列而不是普通队列?


因为阻塞队列可以很好的协调生产者和消费者

  • 若佩奇擀面皮很快,不一会桌子上就满了
  • 阻塞队列:佩奇就休息一下,等面皮被消耗一些之后继续再擀
  • 普通队列:不会停,放不下了也一直擀
  • 若猪爸爸和猪妈妈包的很快,不一会桌子上就空了
  • 阻塞队列:猪爸爸和猪妈妈休息一下,等到面皮擀出来之后再包
  • 普通队列:不会停,没面皮了也一直包

好处

上述生产者消费者模型在后端开发中,经常会涉及到

当下后端开发,常见的结构——“分布式系统”,不是一台服务器解决所有问题,而是分成了多个服务器,服务器之间相互调用

主要有两方面的好处

1. 服务器之间解耦合

我们希望见到“低耦合”

  • 模块之间的关联程度/影响程度

通常谈到的“阻塞队列”是代码中的一个数据结构

但是由于这个东西太好用了,以至于会把这样的数据结构单独封装成一个服务器程序,并且在单独的服务器机器上进行部署

此时,这样的饿阻塞队列有了一个新的名字,“消息队列”(Message Queue,MQ)

如果是直接调用

  • 编写 A 和 B 代码中,会出现很多对方服务器相关的代码
  • 并且,此时如果 B 服务器挂了,A 可能也会直接受到影响
  • 再并且,如果后续想加入一个 C 服务器,此时对 A 的改动就很大

如果是通过阻塞队列

  • A 之和队列通信
  • B 也只和队列通信
  • A 和 B 互相不知道对方的存在,代码中就更没有对方的影子
    看起来,A 和 B 之间是解耦合了,但是 A 和队列,B 和队列之间,不是引入了新的耦合吗?
  • 耦合的代码,在后续的变更工程中,比较复杂,容易产生 bug
  • 但消息队列是成熟稳定的产品,代码是稳定的,不会频繁更改。A、B 和队列之间的耦合,对我们的影响微乎其微
  • 再增加 C 服务器也很方便,也不会影响到原有的 A 和 B 服务器
2. “削峰填谷”的效果

通过中间的阻塞队列,可以起到削峰填谷的效果,在遇到请求量激增突发的情况下,可以有效保护下游服务器,不会被请求冲垮

阻塞队列的作用就相当与三峡大坝在三峡的防汛作用

  • A 向队列中写入数据变快了,但是 B 仍然可以按照原有的速度来消费数据
  • 阻塞队列扛下了这样的压力,就像三峡大坝抗住上游的大量水量的压力
  • 如果是直接调用,A 收到多少请求,B 也收到多少,那很可能直接就把 B 给搞挂了
  • 当 A 不再写入数据的时候,但队列中还存有数据,可以继续工给 B
问题
  1. 为啥一个服务器,收到的请求变多,就容易挂?
  • 一台服务器,就是一台“电脑”,上面就提供了一些硬件资源(包括但不限于 CPU,内存,硬盘,网络带宽…)
  • 就算你这个及其配置再好,硬件资源也是有限的
  • 服务器每次收到一个请求,处理这个请求的过程,就都需要执行一系列的代码,在执行这些代码的过程中,就需要消耗一定的硬件资源(CPU,内存,硬盘,网络带宽…)
  • 这些请求小号的总的硬件资源的量,超过了及其能提供的上限,那么此时机器就会出现(卡死,程序直接崩溃等…)
  1. 在请求激增的时候,A 为啥不会挂?队列为啥不会挂?反而是 B 更容易挂呢?
  • A 的角色是一个“网关服务器”,收到客户端的请求,再把请求转发给其他的服务器
  • 这样的服务器里的代码,做的工作比较简单(单纯的数据转发),消耗的硬件资源通常更少
  • 处理一个请求,消耗的资源更少,同样的配置下,就能支持更多的请求处理
  • 同理,队列其实也是比较简单的程序,单位请求消耗的硬件资源,也是比较少见的
  • B 这个服务器,是真正干活的服务器,要真正完成一系列的业务逻辑
  • 这一系列的工作,代码量非常庞大,消耗的时间很多,消耗的系统硬件资源,也是更多的

类似的,像 MySQL 这样的数据库,处理每个请求的时候,做的工作就是比较多的,消耗的硬件资源也是比较多的,因此 MySQL 也是后端系统中,容易挂的部分

对应的,像 Redis 这种内存数据库,处理请求,做的工作远远少于 MySQL,消耗的资源更少,Redis 就比 MySQL 硬朗很多,不容易挂

代价

  1. 需要更多的机器来部署这样的消息队列(小代价)
  2. A 和 B 之间的通信延迟会变长
  • 对于 A 和 B 之间的调用,要求响应时间比较短就不太适合了

每个技术都有优缺点,不能无脑吹,也不能无脑黑

比如:微服务

  • 本质上就是把分布式系统服务拆的更细了,每个服务都很小,只做一项功能
  • 非常适合大公司,部门分的很细
  • 但需要更多的机器,处理请求需要更多的响应时间,更复杂的后端结构,运维成本水涨船高

Java 自带的阻塞队列

阻塞队列在 Java 标准库中也提供了现成的封装——BlockingQueue

  • BlockingQueue 本质上是一个接口,不能直接 new,只能 new 一个类
  • 因为是继承与 Queue,所以 Queue 的一些操作,offerpoll 这些,在 BlockingQueue 中同样可以使用(不过不建议使用,因为都不能阻塞
  • BlockingQueue提供了另外两个专属方法,都能阻塞
  • put——入列
  • take——出队列
BlockingQueue<String> queue = new ArrayBlockingQueue<>(1000);

capacity 指的是容量,是一个需要加上的参数

public class Demo10 {  
    public static void main(String[] args) throws InterruptedException {  
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);  
        queue.put("111");  
        System.out.println("put成功");  
        queue.put("111");  
        System.out.println("put成功");  
        
    }
}
//运行结果
put成功
put成功
put成功
  • 只打印了三个,说明第四次 put 的时候容量不够,阻塞了
public class Demo10 {  
    public static void main(String[] args) throws InterruptedException {  
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);  
        queue.put("111");  
        System.out.println("put 成功");  
        queue.put("111");  
        System.out.println("put 成功");  
        
        queue.take();  
        System.out.println("take 成功");  
        queue.take();  
        System.out.println("take 成功");  
        queue.take();  
        System.out.println("take 成功");  
    }
}
//运行结果
put 成功
put 成功
take 成功
take 成功
  • 由于只有 put 了两次,所以也只有两次 take,随后阻塞住了
public class Demo11 {  
    public static void main(String[] args) {  
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1000);  
  
        Thread t1 = new Thread(() -> {  
            int i = 1;  
            while(true){  
                try {  
                    queue.put(i);  
                    System.out.println("生产者元素"+i);  
                    i++;  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }        
        });        
        Thread t2 = new Thread(() -> {  
            while(true) {  
                try {  
                    Integer i = queue.take();  
                    System.out.println("消费者元素"+i);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }        
        });        
      t1.start();  
        t2.start();  
    }
}
  • 上述程序中,一个线程生产,一个线程消费
  • 实际开发中,通常可能是多个线程生产,多个线程消费

自己实现一个阻塞队列

普通队列

基于数组的队列

实现一个基础的队列

//此处不考虑泛型参数,只是基于 String 进行存储  
class MyBlockingQueue {  
    private String[] data = null;  
    private int head = 0;  
    private int tail = 0;  
    private int size = 0;  
    
    public MyBlockingQueue(int capacity) {  
        data = new String[capacity];  
    }    
    
    public void put(String s) {  
        if(size == data.length) {  
            //队列满了  
            return;  
        }        
        data[tail] = s;  
        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;  
    }
}

阻塞队列

  • 队列为空,take 就要阻塞,在其他线程 put 的时候唤醒
  • 队列未满,put 就要阻塞,在其他线程 take 的时候唤醒
//此处不考虑泛型参数,只是基于 String 进行存储  
class MyBlockingQueue {  
    private String[] data = null;  
    private int head = 0;  
    private int tail = 0;  
    private int size = 0;  
    private Object locker = new Object();  
  
    public MyBlockingQueue(int capacity) {  
        data = new String[capacity];  
    }  
    
    public void put(String s) throws InterruptedException {  
        //加锁的对象,可以单独定义一个,也可以直接就地使用this  
        synchronized (locker) {  
            if (size == data.length) {  
                //队列满了,需要阻塞  
                //return;  
                locker.wait();  
            }            
            data[tail] = s;  
            tail++;  
            if (tail >= data.length) {  
                tail = 0;  
            }            
            size++;  
            //唤醒 take 的阻塞  
            locker.notify();  
        }    
    }  
    
    public String take() throws InterruptedException {  
        String ret = "";  
        synchronized (locker) {  
            if (size == 0) {  
                //队列为空,需要阻塞  
                //return null;  
                locker.wait();  
            }            
            ret = data[head];  
            head++;  
            if (head >= data.length) {  
                head = 0;  
            }            
            size--;  
            //唤醒 put 的阻塞  
            locker.notify();  
        }        
        return ret;  
    }
}


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
23天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
15天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
19天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2570 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
17天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
1天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
153 2
|
19天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1568 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
2天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
926 14
|
16天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
700 9
|
15天前
|
存储 监控 调度
云迁移中心CMH:助力企业高效上云实践全解析
随着云计算的发展,企业上云已成为创新发展的关键。然而,企业上云面临诸多挑战,如复杂的应用依赖梳理、成本效益分析等。阿里云推出的云迁移中心(CMH)旨在解决这些问题,提供自动化的系统调研、规划、迁移和割接等功能,简化上云过程。CMH通过评估、准备、迁移和割接四个阶段,帮助企业高效完成数字化转型。未来,CMH将继续提升智能化水平,支持更多行业和复杂环境,助力企业轻松上云。