阻塞队列是什么?阻塞队列到底有什么用呢?生产者消费者模型是什么?一文带你了解到底~
阻塞队列是什么?想必读者一定很好奇吧~下面我来讲解一下:
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则,就是带有阻塞功能的队列~~
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素
1.1标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可;
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
阻塞队列可以像队列一样,顺序栈实现或者链式栈实现!!!
1.2阻塞队列用处
这个东西非常有用,尤其是写多线程代码的时候,多个线程之间进行数据交互,可以使用阻塞队列简化代码编写,还可以用来编写“生产者消费者模型”;
java标准库中,提供了阻塞队列~~下面用代码举个例子:
public class ThreadDemo5 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello1");
queue.put("hello2");
queue.put("hello3");
queue.put("hello4");
queue.put("hello5");
String result = null;
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
}
}
可见,程序还没有结束,就证明在最后一次take元素的时候进行了阻塞等待!!!
二、生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题;
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取;举例:
过年大家都会吃饺子,那么包饺子需要擀饺子皮,然后包馅;. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 "生产者", 包饺子的人就是 "消费者"擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的)
于是这就构成了一个生产者消费者模型,中间放饺子皮的就好比一个阻塞队列~~
2.1生产者消费者模型的初心
1.可以让上下游模块之间,进行更好的“解耦合”,耦合表示俩个模块之间的关联关系是强还是弱,关联越强,耦合越高
考虑如下场景:
A服务器调用B服务器(A给B发送请求,B给A发送响应)
如果A和B直接通信,此时就是耦合比较高的情况,如果B挂了,那么A也可能会直接挂;
那么如果引入生产者消费者模型,耦合就降低了~~
A不知道B存在,B也不知道A存在,他俩只认识阻塞队列,此时谁挂了都没有影响
2.削峰填谷
A收到的用户请求多了,有的情况下会出现“峰值情况”,那么有了阻塞队列就可以帮B承担住压力,此时B仍然可以按照之前的速率来取元素;
假设流量波峰后面还有个波谷,此时B仍然可以按照原有速率响应~~
三、模拟实现阻塞队列
- 通过 "循环队列" 的方式来实现.
- 使用 synchronized 进行加锁控制.(线程安全)
- put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
- take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)(阻塞功能)
要注意怎么区分队列是空还是满!!!此处使用size来记录元素个数
//实现阻塞队列
class MyBlockingQueue{
private int[] items = new int[1000];
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length){
this.wait();
}
items[tail] = elem;
tail++;
if (tail == items.length){
tail = 0;
}
size++;
this.notify();
}
synchronized public Integer take() throws InterruptedException {
while (size == 0){
this.wait();
}
int value = items[head];
head++;
if (head == items.length){
head = 0;
}
size--;
this.notify();
return value;
}
}
注意这俩个wait不可能同时阻塞
四、模拟实现生产者消费者模型
创建俩个线程,一个生产,一个实现
public class ThreadDemo {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//消费者
Thread t1 = new Thread(() -> {
while (true){
try {
int value = queue.take();
System.out.println("消费:"+ value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true){
System.out.println("生产:"+value);
try {
queue.put(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
value++;
}
});
t1.start();
t2.start();
System.out.println("hello");
}
}
//实现阻塞队列
class MyBlockingQueue{
private int[] items = new int[1000];
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length){
this.wait();
}
items[tail] = elem;
tail++;
if (tail == items.length){
tail = 0;
}
size++;
this.notify();
}
synchronized public Integer take() throws InterruptedException {
while (size == 0){
this.wait();
}
int value = items[head];
head++;
if (head == items.length){
head = 0;
}
size--;
this.notify();
return value;
}
}