文章目录
- 前言
- 1. 什么是阻塞队列
- 2. 生产者消费者模型
- 2.1 生产者消费者模型的优势
- 2.1.1 解耦合
- 2.1.2 削峰填谷
- 3. Java 标准库中的阻塞队列
- 3.1 生产者消费者模型
- 4. 自己实现一个阻塞队列
- 总结
前言
本文主要给大家讲解多线程的一个重要案例 — 阻塞式队列.
关注收藏, 开始学习吧🧐
1. 什么是阻塞队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出”(First In First Out) 的原则.
阻塞队列是线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
而阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型, 作为一种处理多线程问题的方式.
2. 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题.
生产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯, 所以生产者生产完数据之后不用等待消费者处理, 直接扔给阻塞队列, 消费者不找生产者要数据, 而是直接从阻塞队列里取.
比如过年一家人一起包饺子. 并不是由每一个人既负责擀饺子皮又负责包饺子, 这种做法是比较低效的.
一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 在这里我们假设擀饺子皮的人就是 “生产者”, 那么包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).
2.1 生产者消费者模型的优势
简单来说, 生产消费者模型可以减少任务切换所花费的开销, 并且减少不必要的锁竞争.
优势主要有以下两个:
- 解耦合
- 削峰填谷
2.1.1 解耦合
阻塞队列可以使生产者和消费者之间 解耦. 解耦就是降低模块之间的耦合. 现在我们来考虑一个分布式系统.
在这种情况下, A 是直接将外网发送来的请求转发给 B, A 和 B 之间的耦合就比较明显了.
- 如果 B 挂了, 就可能对 A 造成很大的影响. 反过来, 如果 A 挂了, 也会对 B 造成很大影响.
- 再添加一个服务器 C, 此时还需要对 A 的代码进行比较大的改动.
如果引入生产者消费者模型, 引入一个阻塞队列, 就能有效的解决以上问题.
此时, A 和 B 就可以通过阻塞队列很好地解耦合了.
- 如果 A 或者 B 挂了, 由于他们彼此之间不会直接进行数据交换, 没有什么太大的影响.
- 如果要新增一个服务器 C, A 也不需要进行任何代码上的修改, 直接让 C 从阻塞队列中取元素即可.
2.1.2 削峰填谷
阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力.
服务器收到的来自于用户的请求, 并不是一成不变的, 经常会因为一些突发事件而导致用户请求数目暴增. 这种场景有很多, 比如大学中的线上选课, 微博突然有某位明星公布恋情等等, 都在短时间内产生了大量客户端用户层面上的请求.
我们还是举一个分布式系统的例子.
此时 A 每次收到一个请求, B 也就需要立即处理一个请求. 如果 A 能够承受的压力大, B 承受的压力比 A 小的话, 此时很有可能 B 先挂了.
这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.
当外界的请求突然增多, A 收到的请求变多了, A 就会给阻塞队列中写入更多的请求数据. 而 B 仍可以按照之前的速度来处理请求, 就不会发生被直接冲垮的情况了.
- 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.
- 峰值很多时候都是暂时的, 当峰值消退的时候, A收到的请求就变少了, B 还是按照既定的速度进行 “填谷”, 也不至于太空闲.
3. Java 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可. 有基于链表的, 有基于堆的, 有基于数组的.
基于数组的 Array 这个版本的速度要更快, 但前提是得先知道最多有多少个元素. 如果不知道有多少元素, 使用 Linked 更合适, 毕竟对于 Array 版本来说, 频繁扩容也会是一个不小的开销.
使用时要注意以下三点:
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
接下来我们就可以基于阻塞队列来实现一个简单的生产者消费者模型.
3.1 生产者消费者模型
我们写俩个线程, 一个线程用来生产, 一个线程用来消费.
public class ThreadDemo18 {
// 代码, 让生产者, 每隔 1s 生产一个元素.
// 让消费者则直接消费, 不受限制.
public static void main(String[] args) {
// 创建一个基于链表实现的阻塞队列.
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
Thread t1 = new Thread(() -> {
while (true) {
try {
int ret = queue.take();
System.out.println("消费元素: " + ret);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "consumer");
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产元素: " + value);
queue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "producer");
t1.start();
t2.start();
}
}
4. 自己实现一个阻塞队列
学习了之前的基础内容, 接下来, 我们将自己实现一个阻塞队列.
- 通过 “循环队列” 的方式来实现.
- 使用 synchronized 进行加锁控制.
- put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
- take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
class MyBlockingQueue {
// 使用一个 int 类型的数组来保存元素. 假设这里只存 int.
private int[] items = new int[100];
// head 指向队伍的头部
volatile private int head = 0;
// tail 指向队伍的尾部
volatile private int tail = 0;
// size 用来记录元素个数
volatile private int size = 0;
// 创建一个 locker 锁对象
private Object locker = new Object();
// 入队列
public void put(int elem) throws InterruptedException {
synchronized (locker) {
while (size >= items.length) {
// return;
locker.wait();
}
items[tail++] = elem;
if (tail >= items.length) {
tail = 0;
}
size++;
locker.notify();
}
}
// 出队列
public int take() throws InterruptedException {
synchronized (locker) {
while (size == 0) {
// return 0;
locker.wait();
}
int elem = items[head++];
if (head >= items.length) {
head = 0;
}
size--;
locker.notify();
return elem;
}
}
}
总结
✨ 本文主要讲解了阻塞队列的概念, 以及基于阻塞队列实现的生产着消费者模型, 并自己动手写了一个阻塞队列.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!