Java【多线程基础5】阻塞队列的原理和使用方式 + 模拟实现BlockingQueue


前言

📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙 希望我的专栏能够帮助到你:
JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 堆, 二叉树, 二叉搜索树, 哈希表等
JavaEE初阶: 多线程, 网络编程, TCP/IP协议, HTTP协议, Tomcat, Servlet, Linux, JVM等(正在持续更新)

上篇多线程基础4主要介绍了: 单例模式中的饿汉模式和懒汉模式 , 同时在多线程环境下, 对可能造成的线程安全问题做出了改进

本篇继续介绍多线程相关的基础内容, 内容较多, 分为若干篇持续分享


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

一、阻塞队列

1, 什么是 阻塞队列

📌阻塞队列 是一种带有阻塞功能的, 线程安全的队列, 也遵守 “先进先出” 原则

👉如果队列为空, 则不出队, 进入堵塞状态, 等其他线程插入数据, 队列不为空时再出队
👉如果队列为满, 则不入队, 进入堵塞状态, 等其他线程删除数据, 队列不为满时再入队

2, 如何使用 阻塞队列

Java集合中封装了阻塞队列这种数据结构, 即: BlockingQueue 接口, 其具体实现类即有顺序存储形式, 也有链式存储形式

入队列操作为 put , 出队列操作为 take, 不提供查看队首元素的操作

使用顺序存储的阻塞队列, 容量设置为 5 , 循环插入数据 5 次, 退出循环再插入一次数据, 打印每次插入的数据

        // 顺序存储的阻塞队列
        BlockingQueue<Integer> queue1 = new ArrayBlockingQueue<>(5);
        // 链式存储的阻塞队列
        BlockingQueue<Integer> queue2 = new LinkedBlockingQueue<>();
        int n = 5;
        while(n > 0) {
            queue1.put(n);
            System.out.println("插入: " + n);
            n--;
        }
        queue1.put(100);
        System.out.println("插入: " + 100);

预期结果 : 循环结束后, 队列为满, 此时如果再想插入数据, 该线程就会进入堵塞状态

来看执行结果 :

在这里插入图片描述

符合预期, 并没有打印出最后一个想插入的数据

阻塞队列最常使用的场景是 , 生产者消费者模型✅


二、生产者消费者模型

1, 什么是 生产者消费者模型

📌A 负责产出, B 负责消耗, A 和 B 不直接通讯, 而是通过一个容器进行通讯

👉例如, 小明和小红要包饺子~ 小明负责擀饺子皮, 擀好的饺子皮放在案板上, 小红负责包饺子 , 那么小明就是生产者, 小红就是消费者, 案板就是进行通讯的容器

👉在程序中, 生产者和消费者彼此之间通过阻塞队列来进行通讯, 所以生产者生产完数据后不关心消费者, 直接扔给阻塞队列, 消费者不关心生产者, 只要阻塞队列里有, 就从阻塞队列中取数据


2, 生产者消费者模型 的作用

使用生产者消费者模型有两个好处
1️⃣ 能够进行 “解耦合” 耦合是指: 两个事物的关联性强弱

例如, 现有 A 和 B 两个服务器, 两个服务器之间进行交互, A 给 B 发送请求, B 给 A 返回响应, 此时 A 和 B 就是高耦合, 如果 B 服务器崩了, A 会受到直接影响, 可能 A 也会崩

此时如果想再让 A 多连接一台服务器 C , A 和 C 也进行交互, 这就需要对 A 做出很多调整
在这里插入图片描述
改善这一问题, 就可以使用生产者消费者模型, 给 A B 两个服务器之间引入一个阻塞队列, 再扩充上其他的功能变成服务器, 就成为了一个 “消息队列服务器”

AB通过阻塞队列来通讯, 互相不直接关联, 即便B挂了也不会影响A, 若想再链接一台 C 服务器, 只需要链接这个消息队列服务器即可, 对 A 的影响也不大

在这里插入图片描述

2️⃣ 可以"削峰填谷"
还是以刚才的 A B 服务器为例, 如果某一时刻 A 给 B 发送的请求达到峰值, 随即又骤降到谷值, 由于 A B 服务器还有其他的业务压力, B 服务器处理请求时也是需要消耗资源的, 如果请求太多, B 服务器可能就挂了, 会对系统稳定性造成风险

如果使用生产者消费者模型, A B 通过阻塞队列来缓解压力, 阻塞队列没有业务压力, 也更加稳定✅

A 给 B 服务器发送请求的速率, B 处理 A 的请求的速率就可以达到平衡, 可以避免某一刻速率太高, 下一刻速率太低的情况


3, 阻塞队列 结合 生产者消费者模型

创建两个线程, thread1 作为消费者, thread2 作为生产者

消费者负责在阻塞队列中删除数据, 生产者负责在阻塞队列中增加数据, 每次生产和消费之后休眠一秒, 并进行打印, 方便观察执行情况

        BlockingQueue<Integer> queue1 = new ArrayBlockingQueue<>(5);
		// 消费者
        Thread thread1 = new Thread( () -> {
            while(true) {
                try {
                    int value = queue1.take();
                    Thread.sleep(1000);
                    System.out.println("消费 : " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 生产者
        Thread thread2 = new Thread( () -> {
            int value = 0;
            while(true) {
                try {
                    queue1.put(value);
                    Thread.sleep(1000);
                    System.out.println("生产 : " + value);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();

执行结果 :
在这里插入图片描述


三、模拟实现阻塞队列

上面已经了解过, 阻塞队列是一种线程安全的, 具有阻塞效果的队列

所以模拟实现队列分为三步 :
1️⃣ 实现一个队列(这里采用顺序存储的环形队列)
2️⃣ 保证线程安全
3️⃣ 实现阻塞效果

👉普通的环形队列代码实现:

	private int[] array = new int[100];// 数组
    private int size = 0;// 实际长度
    private int front = 0;// 头
    private int rear = 0;// 位
    
    public void put(int value) throws InterruptedException {
    	// 判断队列是否为满
        if(size == array.length) {
            return;
        }
        array[rear++] = value;
        // 尾走到最后要从头开始(环形)
        if(rear == array.length) {
            rear = 0;
        }
        size++;
    }
    
    public Integer take() throws InterruptedException {
    	// 判断队列是否为空
        if(size == 0) {
            return null;
        }
        int value = array[front];
        this.front++;
        // 走到最后要从头开始(环形)
        if (front == array.length) {
            front = 0;
        }
        size--;
        return value;
    }

👉接下来, 要改进一下, 保证线程安全
1️⃣ 修改操作要保证原子性, put和take方法都有修改数据的操作, 所以这两个方法都直接加锁, 用 synchronized 修饰
2️⃣ 读操作要保证满足内存可见性, 所以 size, front 和 rear都加上 volatile 修饰

👉最后, 要加上阻塞效果, 如果队列为满, 不能再入队, 如果队列为空, 不能再出队, 所以在 put 和 take 方法中的判空, 判满处的 return 全部改成 wait 方法, 并且在最后加上 notify 方法

⚠️⚠️⚠️注意 :
put 方法里判满时的 wait , 是由 take 方法最后的 notify 唤醒, take 里判空时的 wait , 是由 put方法最后的 notify 唤醒, put 和 take 不可能同时进入阻塞状态

private int[] array = new int[100];// 数组
    volatile private int size = 0;// 实际长度
    volatile private int front = 0;// 头
    volatile private int rear = 0;// 位

    synchronized public void put(int value) throws InterruptedException {
        if(size == array.length) {
        	// 阻塞等待
            this.wait();
        }
        array[rear++] = value;
        if(rear == array.length) {
            rear = 0;
        }
        size++;
        this.notify();
    }
    synchronized public Integer take() throws InterruptedException {
        if(size == 0) {
        	// 阻塞等待
            this.wait();
        }
        int value = array[front];
        this.front++;
        if (front == array.length) {
            front = 0;
        }
        size--;
        this.notify();
        return value;
    }

👉你以为结束了吗? 不, 还没有

wait方法是可以被外部的 interrupt 方法打断的, 而不是被 notify 唤醒, 此时代码就可能就破坏了阻塞特性, 所以要把 if 换成 while , 如果不是被 notify 唤醒, 就再判断一下是否满足非空 / 非满这个条件

🚗🚗🚗
最终, 模拟实现的阻塞队列代码如下 :

private int[] array = new int[100];// 数组
    volatile private int size = 0;// 实际长度
    volatile private int front = 0;// 头
    volatile private int rear = 0;// 位

    synchronized public void put(int value) throws InterruptedException {
    	// 重复判断, 避免被 interrupt 打断
        while (size == array.length) {
            this.wait();
        }
        array[rear++] = value;
        if(rear == array.length) {
            rear = 0;
        }
        size++;
        // 唤醒 take 方法中的 wait
        this.notify();
    }
    synchronized public Integer take() throws InterruptedException {
        // 重复判断, 避免被 interrupt 打断
        while (size == 0) {
            this.wait();
        }
        int value = array[front];
        this.front++;
        if (front == array.length) {
            front = 0;
        }
        size--;
        // 唤醒 put 方法中的 wait
        this.notify();
        return value;
    }

总结

以上就是本篇的全部内容, 主要介绍了阻塞队列的原理和模拟实现方式

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

  • 16
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灵魂相契的树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值