一、什么是阻塞队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
了解:
实际开发中使用到的阻塞队列并不是一个简单的数据结构,而是一个专门的服务器程序,他提供阻塞队列的功能,还会在此基础上提供更多,如对数据持久化储存,支持多个数据通道,支持多节点容灾冗余备份,支持管理面板等等,而这样的队列又有新名字,叫信息队列
二、生产者消费者模型
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.如下:
-
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
-
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放
到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮
阻塞队列也能使生产者和消费者之间 解耦
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺
子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的)
三、标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果需要在一些程序使用阻塞队列, 直接使用标准库中的即可.
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
代码:
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
基于标准库中的阻塞队列实现一个生产者消费者模型:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//基于这个内置的阻塞队列实现 生产者消费者模型
public class BlockingQueueTest{
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
Thread producer = new Thread(() -> {
int num = 0;
while (true){
System.out.println("生产了" + num);
try {
queue.put(num);
num++;
Thread.sleep(500);//当生产的慢一些话,消费者就会与生产者同步,生产多少消费多少
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread customer = new Thread(() -> {
while (true){
try {
int ret = queue.take();
System.out.println("消费了" + ret);
//Thread.sleep(500); 当消费的慢一些话,就是生产的很快,消费不过来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
customer.start();
}
}
四、自己实现一个阻塞队列
- 通过 “循环队列” 来实现.(基于数组实现入队列和出队列操作,注意判断队列为空和为满情况)
实现循环队列有一个重要的问题,如何区分是空队列还是满队列,如果不加额外限制,此时空和满的条件都是head和tail重合,所以我们使用以下方法:
1.浪费一个空间,当head == tail,即为空。head == tail+1,即为满
2.创建一个变量 size 记录元素个数,size==0,即为空。size == arr.length,即为满当前这里我们采用第二种方法:
入队列时,把新元素放到tail位置上,tail++,并判断tail是否到达数组末尾,若到达tail=0重新循环,若没有size++。出队列时,就把head位置的元素返回,head++,并判断head是否到达数组末尾,若到达返回0,若没有size- -
- put和take操作公共变量,使用 synchronized 进行加锁控制. 针对哪个对象加锁就使用哪个对象wait和notify.
- 使用 wait 和 notify 来实现阻塞效果:
1.put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
2.take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
代码如下:
class MyBlockingQueue{
private int[] data = new int[1000];
//数组有效个数
private int size = 0;
//队首下标
private int head = 0;
//队尾下标
private int tail = 0;
//入队列
synchronized public void put(int value) throws InterruptedException {
if (size == data.length){
//队列满了,暂时先直接返回
//return;
this.wait();//put中的wait要由take来唤醒:只要take成功,队列就不为满
}
//把新的元素放到tail位置
data[tail] = value;
tail++;
//处理tail到达末尾的情况
//tail = tail % data.length; 不建议使用
if (tail >= data.length){
tail = 0;//从头开始
}
size++;
this.notify();//put成功后就唤醒 take中的wait
}
//出队列
synchronized public Integer take() throws InterruptedException {
if (size == 0){
//如果队列为空,就返回一个非法值
//return null;
this.wait();//take 中的等待要由 put来唤醒:当put成功后,队列就不为空。
}
//取出head位置的元素
int ret = data[head];
head++;
if (head >= data.length){
return 0;
}
size--;
//take成功后就唤醒put中的wait
this.notify();
return ret;
}
}
注意: 理解清楚此处阻塞机制如何实现的
当前代码中,put 和 take 两种操作不会同时 wait,因为阻塞条件是不同的。当队列满 put 开始wait阻塞等待,当队列空 take 开始阻塞等待
测试该手动实现的阻塞队列:
public class BlockingQueueTest{
//用自己实现的阻塞队列MyBlockingQueue来完成 生产者消费者模型
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
Thread producer = new Thread(() -> {
int num = 0;
while (true){
System.out.println("生产了" + num);
try {
queue.put(num);
num++;
//Thread.sleep(500);//当生产的慢一些话,消费者就会与生产者同步,生产多少消费多少
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread customer = new Thread(() -> {
while (true){
try {
int ret = queue.take();
System.out.println("消费了" + ret);
Thread.sleep(500); //当消费的慢一些话,就是生产的很快,后面消费一个才会生产一个,也有可能消费两个生产两个
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
customer.start();
}
}