【java】阻塞队列以及它的实现
本文介绍了阻塞队列的概念及其在多线程环境中的应用。阻塞队列是一种线程安全的队列,遵循先进先出原则,具有当队列满时阻塞入队、队列空时阻塞出队的特性。文章重点阐述了生产者消费者模型如何通过阻塞队列实现解耦,并举例说明其在秒杀场景中削峰填谷的作用。此外,详细解析了Java标准库中的BlockingDeque接口使用,并提供了一个自定义阻塞队列的实现方案,包括循环队列结构、wait/notifyAll机制
1 概念
阻塞队列是一种特殊的队列,也同样拥有 “ 先进先出 ” 的特点。
阻塞队列是一种线程安全的队列,并有着以下特点:
- 当队列为满时,如果继续入队就会阻塞,直到有其他线程从队列中取走元素。
- 当队列为空时,如果继续出队就会阻塞,直到有其他线程从队列中放入元素。
阻塞队列这种模型就是 “ 生产者消费者模型 ”,这是一种非常典型的开发模型。
2 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.(削峰填谷)
比如在"秒杀"场景下,服务器同一时刻可能会收到大量的支付请求.如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程).这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求.
这样做可以有效进行"削峰",防止服务器被突然到来的一波请求直接冲垮.
2. 阻塞队列也能使生产者和消费者之间解耦.
3 标准库中的阻塞队列
java的标准库中就提供了阻塞队列,如果我们需要在一些程序中使用阻塞队列,直接使用java标准库的阻塞队列即可:
public static void main(String[] args) throws InterruptedException {
BlockingDeque<Integer> queue=new LinkedBlockingDeque<>();
//放入元素
queue.put(32);
//取出元素
int num=queue.take();
System.out.println(num);
}
- BlockingDeque 只是一个接口,真正实现阻塞队列的是 LinkedBlockingDeque类。
- put 方法是放入元素,take 方法是取出元素。
- LinkedBlockingDeque 类中还有 offer、poll、peek方法,但这些方法都不具有阻塞特性。
4 阻塞队列实现
模拟实现一个可以存放Integer类型的阻塞队列,代码如下:
class MyBlockingQueue{
private int[] array=null;//数组
int head=0;//队首
int tail=0;//队尾
int size=0;//长度
public MyBlockingQueue(int volume){
array=new int[volume];
}
//入队
public void put(int elem)throws InterruptedException{
synchronized (this){
while(size>=array.length){
wait();
}
array[tail++]=elem;
//循环队列
if(tail>=array.length){
tail=0;
}
size++;
notifyAll();
}
}
//出队
public Integer take()throws InterruptedException{
int elem=0;
synchronized (this){
while(size==0){
wait();
}
elem =array[head];
head++;
//循环队列
if(head>=array.length){
head=0;
}
size--;
notifyAll();
}
return elem;
}
}
4.1 通过" 循环队列 ''的方式
为了防止数组移位而造成的空间资源浪费("假溢出"),高效复用数组。
4.2 wait 搭配 while 循环
wait() 阻塞需要搭配while(判断条件)来使用,以防止线程被虚假唤醒,从而导致线程逻辑乱套。何为虚假唤醒?
✅虚假唤醒:
一句话解释就是,wait 没有被 notify / notifyAll 唤醒,自己莫名其妙就醒了,这是JVM操作系统底层自带的,是正常现象,不是程序 Bug。
如果用 if 判断:
//入队
if(size>=array.length){
wait();
}
//出队
if(size==0){
wait();
}
假设此时wait 自己醒了,但此时队列是满/空的,而 if 只判断一次是否队满/空,程序就会继续执行下面的入队操作,就会造成逻辑混乱、数组越界、并发异常。
所以需要套用 while 循环进行二次校验,是否满足条件,若满足队满 / 队空,则继续堵塞。
//入队
while(size>=array.length){
wait();
}
//出队
while(size==0){
wait();
}
4.3 使用 notifyAll 方法 唤醒
notify只能唤醒一个线程,如果每次唤醒都只唤醒一个线程的话,就可能造成" 线程饿死 "的现象
✅线程饿死:
多个线程阻塞等待时,notify 每次只唤醒一个线程,会导致有的线程始终都没有被叫醒过一次,一致处于阻塞等待中,得不到执行机会,活活被 “ 饿死 ”。
故使用 notifyAll 唤醒所有线程,谁满足条件谁就执行。
4.4 synchronized 关键字保证线程安全
-
入队put操作阻塞时,在多线程下,被唤醒时可能队列还是满的,因为notify可能唤 醒多个线程。
-
出队take操作阻塞时,在多线程下,被唤醒时可能队列还是空的,因为notify可 醒多个线程。
总之,标准的阻塞队列需要 " 循环队列 + while 循环 + notifyAll唤醒 + synchronized 加锁 " 团结合作,才能保证多线程安全。
关于阻塞队列的相关知识
更多推荐

所有评论(0)