一、阻塞队列和消费者模型
1.1 什么是阻塞队列?
基本概念:阻塞队列是一种特殊的队列,也遵守 “先进先出” 的原则。
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。
其中,阻塞队列的一个典型应用场景就是 “生产者消费者模型”。 这是一种非常典型的开发模型。
1.2 消费者模型
生产者、消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
- 阻塞队列也能使生产者和消费者之间 解耦。
比如在 “购物秒杀” 场景下,服务器同一时刻可能会收到大量的支付请求。如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求。
这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮。
二、标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列,我们在日常使用时,可以直接调用标准库中的阻塞队列。其中,
- BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue。
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列。
- BlockingQueue 也有 offer,poll, peek 等方法,但是这些方法不带有阻塞特性。
BlockingQueue<string> queue = new LinkedBlockingqueue<>();
queue.put("abc");// 入队列
String elem = queue.take();// 出队列.如果没有 put 直接 take,就会阻塞
此外,BlockingQueue还单独扩展了一些特有的方法,内容如下:
三、阻塞队列的实现(重点)
3.1 通过 “循环队列” 的方式来实现
其中,[head, tail) 构成了一个区间,这个区间里的内容就是当前队列中的有效元素。入队列,把新的元素,,放到 tail 位置上,同时 tail++;出队列,把 head 指向的元素给删除掉,head++。
初始情况下,队列为空的,head 和 tail 重合的;当队列满了的时候,
head 和 tail 又重合。
解决方案:
- 浪费一个位置, 让 tail 指向 head 的前一个位置,就算满了
- 专门搞一个变量,size,来表示元素个数,size 为 0 就是 空,为 数组最大值,就是满。
(更多关于队列的内容见前面的数据结构专栏)
3.2 使用 synchronized 进行加锁控制.
3.3 put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait,被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
阻塞队列put操作的相关实例代码:
public void put(String elem) throws InterruptedException {
synchronized (locker){
//1、期望wait返回之后,在判定一次条件
//2、条件仍然成立,继续wait
//3、如果不成立,才往后执行
while (size >= data.length) {
locker.wait(); //如果队列满了,继续插入元素,就会发生阻塞
}
data[tail] = elem; //队列没满,真正的往里面添加元素
tail++;
if (tail >= data.length) {
tail = 0;
}
size++;
locker.notify(); //这个notify,用来唤醒take中的wait
}
}
3.4 take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
阻塞队列take操作的相关实例代码:
public String take() throws InterruptedException {
synchronized (locker){
while (size == 0){
locker.wait(); //队列空了,如果继续出元素,就会发生阻塞
}
//队列不空,就可以将队首元素(head位置的元素)删除掉,并进行返回
String ret = data[head];
head++;
if (head >= data.length){
head = 0;
}
size--;
locker.notify(); //这个notify 用来唤醒put中的wait
return ret;
}
}
3.5 用阻塞队列实现消费者模型总的示例代码:
package BlockingQueue;
/**
* @author Zhang
* @date 2024/5/816:52
* @Description:
*/
public class MyBlockingQueue {
// 此处这里的最大长度,也可以指定构造方法,由构造方法的参数来制定
private String[] data = new String[1000];
//队列的起始位置
private volatile int head = 0;
//队列的结束位置的下一个位置
private volatile int tail = 0;
//队列中有效元素的个数
private volatile int size = 0;
private final Object locker = new Object();
/**
* 核心方法,元素入队列
* @param elem
* @throws InterruptedException
*/
public void put(String elem) throws InterruptedException {
synchronized (locker){
//1、期望wait返回之后,在判定一次条件
//2、条件仍然成立,继续wait
//3、如果不成立,才往后执行
while (size >= data.length) {
locker.wait(); //如果队列满了,继续插入元素,就会发生阻塞
}
data[tail] = elem; //队列没满,真正的往里面添加元素
tail++;
if (tail >= data.length) {
tail = 0;
}
size++;
locker.notify(); //这个notify,用来唤醒take中的wait
}
}
/**
* 元素出队列
* @return
* @throws InterruptedException
*/
public String take() throws InterruptedException {
synchronized (locker){
while (size == 0){
locker.wait(); //队列空了,如果继续出元素,就会发生阻塞
}
//队列不空,就可以将队首元素(head位置的元素)删除掉,并进行返回
String ret = data[head];
head++;
if (head >= data.length){
head = 0;
}
size--;
locker.notify(); //这个notify 用来唤醒put中的wait
return ret;
}
}
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
//生产者
Thread producer = new Thread(()->{
int num = 0;
while (true){
try {
queue.put(num+"");
System.out.println("生产元素:"+num);
num++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread customer = new Thread(()->{
while (true){
try {
String result = queue.take();
System.out.println("消费元素:"+result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
customer.start();
// customer.join();
//producer.join();
}
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了什么是阻塞队列,生产者消费者模型,并且介绍了标准库中的阻塞队列,以及阻塞队列的具体实现代码。