文章目录
前言
📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙 希望我的专栏能够帮助到你:
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;
}
总结
以上就是本篇的全部内容, 主要介绍了阻塞队列的原理和模拟实现方式
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~
上山总比下山辛苦
下篇文章见