阻塞队列
什么是阻塞队列
说到队列, 第一时间想到的队列的特性就是: 一种 “先进先出” 的数据结构
而阻塞队列是一种特殊的队列, 也继承了队列的特性: 先进先出.
- 但是在其先进先出的特性的基础上, 阻塞队列拥有另一种特殊的功能, 顾名思义, 就是: 阻塞
它在普通队列的基础上额外拥有以下几个特性:
- 如果队列为空, 执行出队列的操作, 就会阻塞, 阻塞到另一个线程往里面添加元素为止 (队列不空)
- 如果队列满了, 执行入队列的操作, 也会阻塞, 阻塞到另一个线程从里面取走元素为止 (队列不满)
BlockingQueue 的使用
Java 标准库中提供了一个接口 (BlockingQueue), 其中包含了几种阻塞队列的实现:
可以看到, 标准库中的阻塞队列有好几种实现方式, 有基于数组实现的 ArrayBlockingQueue, 有基于链表实现的 LinkedBlockingQueue, 有带有优先级的阻塞队列 PriorityBlockingQueue 等等…
阻塞队列也给我们提供了具体的操作阻塞队列的方法:
可以看到, 标准库提供的阻塞队列中带有阻塞作用的方法是 put() 和 take()
- 注意: offer() 和 poll() 是不带有阻塞的方法
下面简单的使用一下标准库的阻塞队列:
public class ThreadDemo21 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String > blockingQueue = new LinkedBlockingQueue<>();
blockingQueue.put("1");
blockingQueue.put("2");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
}
上述代码中, 使用 put() 方法往队列中添加了 “1” 和 “2” 两个元素, 并使用 take() 方法取出元素三次并打印
可以看到控制台中打印了两个元素, 并且产生了阻塞的效果, 如果当前有另一个线程往队列中添加元素的时候, 这个程序就能立刻取出第三个元素并打印
使用 BlockingQueue 来完成生产者消费者模型
生产者-消费者模型是一个经典的多线程模型, 由两类线程和一个缓冲区组成
- 生产者作为一类线程, 负责生产数据, 并将数据放入缓冲区中
- 缓冲区作为一组中间状态的组件, 可以使用阻塞队列来作为缓冲区, 或者使用一些中间件, 如: 消息队列 (Kafka, RabbitMQ…)
- 消费者作为另一类线程, 负责消费数据. 消费者会从缓冲区接收数据并将接收的数据进行消费
这里使用 BlockingQueue 来作为缓冲区完成基本的生产者-消费者模型:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo22 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
// 创建两个线程, 来作为生产者和消费者
Thread customer = new Thread(() -> {
while (true) {
try {
Integer res = blockingQueue.take();
System.out.println("消费元素: " + res);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(() -> {
int count = 0;
while (true) {
try {
blockingQueue.put(count);
System.out.println("生产元素: " + count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
上述代码, 创建了两个线程 t1 和 t2 分别作为生产者和消费者线程, 使用 blockingQueue 来作为数据缓冲区, t2 负责生产数据, 每隔 500ms 往队列中添加一个元素, 而 t1 负责消费数据, 会不间断的向队列索要元素, 当 t2 往队列中添加元素的时候, t1 就会直接把这个元素取走 (当然, 消费者消费的速度是可以通过程序来控制的)
当运行上述程序之后, 可以看到, 生产者源源不断的生产出数据, 而消费者也会取走数据. 生产者, 消费者, 缓冲区 这三个元素共同构建出了生产者-消费者模型
自定义阻塞队列
想要实现一个阻塞队列, 首先需要先写一个普通的队列.
队列可以基于数组或者链表来实现, 使用链表更容易进行头插和尾删的操作.
这里使用数组来实现一个循环队列:
- 定义一个 item[] 数组, 数组长度默认为 100
- 使用 head 和 tail 两个节点分别指向头结点和尾结点
- 使用 size 来存储数组中存放了多少个元素
- 当 head 跟 tail 指向的结点为同一个结点的时候, 如果此时 size == 0 队列为空, 此时 size == item.length 的话队列为满.
基于上述属性来写一个循环队列:
class MyBlockingQueue {
private int[] items = new int[100];
private int head = 0;
private int tail = 0;
private int size = 0;
// 入队列
public void put(int val) throws InterruptedException {
if (size == items.length) {
// 队列满了
return;
}
items[tail] = val;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
}
// 出队列
public Integer take() throws InterruptedException {
int res = 0;
if (size == 0) {
// 队列空
return null;
}
res = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
return res;
}
}
基于上面所写的循环队列, 可以将它升级为阻塞队列, 需要给它加入阻塞功能
加入阻塞功能, 就代表着需要在多线程环境下来使用这个队列, 由于里面的入队列和出队列方法都涉及到读写操作, 所以就需要给这两个方法加上锁
接下来给这个队列加入阻塞功能:
- 使用 wait 和 notify 来互相配合
- 当检测到队列为空的时候要执行出队列操作, 就使用 wait 使出队列操作阻塞, 当有另外的线程往里面添加元素的时候, 使用 notify 通知刚才的 wait, 让它在队列非空的情况下执行出队列操作
- 当检测到队列为满的时候要执行入队列操作, 就是用 wait 使入队列操作阻塞, 当有另外的线程往里面取走元素的时候, 使用 notify 通知刚才的 wait, 让它在队列不满的情况下执行入队列操作
这里有一个小问题, 有没有可能, 当入队列之后通知出队列的线程执行出队列的操作的时候, 这个时候会不会出现队列还是空的情况呢?
当然在这个程序中是不可能出现这种情况的, 但是作为程序员, 为了稳妥起见, 最好的办法是在 wait 返回之后再次判断条件是否具备
可以看一下标准库中 wait 的注释, 可以看到标准库中推荐的写法:
- 使用 while 循环来判定是否满足条件, 不满足则继续进行 wait 操作
这样, 我们自定义的阻塞队列就大功告成了:
class MyBlockingQueue {
private int[] items = new int[100];
private int head = 0;
private int tail = 0;
private int size = 0;
// 入队列
public synchronized void put(int val) throws InterruptedException {
while (size == items.length) {
// 队列满了
// return;
// 加入阻塞操作
this.wait();
}
items[tail] = val;
tail++;
// 针对 tail 的处理
if (tail >= items.length) {
tail = 0;
}
size++;
// 唤醒 take 中的 wait
this.notify();
}
// 出队列
public synchronized Integer take() throws InterruptedException {
while (size == 0) {
// 队列空
// return null;
// 加入阻塞操作
this.wait();
}
int res = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
// 唤醒 put 中的wait
this.notify();
return res;
}
}
使用自定义阻塞队列来完成生产者消费者模型
public class ThreadDemo23 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
try {
int res = queue.take();
System.out.println("消费元素: " + res);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(() -> {
int count = 0;
while (true) {
try {
System.out.println("生产元素: " + count);
queue.put(count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
得到结果: