要想学习并实现阻塞队列,这里我们要先搞懂什么是生产者-消费者模型
生产者-消费者模型:
在计算机科学中,生产者-消费者模型是一种经典的多线程/多进程同步问题 。它描述了一组生产者线程和一组消费者线程,它们通过一个**共享的、固定大小的缓冲区(通常是 FIFO 队列)**进行通信和协作
在这个模型中,有三个核心组成部分和三种关键关系:
- 生产者(Producer):生成数据或任务,并将其放入共享缓冲区。如果缓冲区满了,它必须等待,直到缓冲区中有空位出现
- 消费者(Consumer):从缓冲区中取出数据并进行处理。如果缓冲区空了,它必须等待,直到缓冲区中有任务存在
- 缓冲区(Buffer):作为中间的通信通道,屏蔽了生产者和消费者的细节。对它的操作必须是线程安全的
具体了解缓冲区是什么,有什么用:
生产者生产任务,消费者负责消耗任务,这很好理解,那么什么是缓冲区呢?我想以一个简单的例子为例:
我们可以将这个模型结合"三峡大坝"来看,来看看缓冲区的作用是什么
我们先介绍三峡大坝体系中的生产者与消费者:
·生产者: 在长江上游,江水(数据/任务)时而汹涌澎湃,时而平缓流淌,这就像我们的**生产者线程,**是不稳定的
·消费者: 而在大坝下游,发电机组需要以恒定的速度转动发电,这就像我们的消费者线程,处理能力往往是有限且固定的
想要知道缓冲区(也就是大坝)的作用是什么,我们可以观察若没有缓冲区存在会出现什么情况:
- 洪水期(生产过快): 下游的发电机根本来不及处理巨大的水量,如果不加控制,下游就会发生洪涝灾害(内存溢出或系统崩溃)
- 枯水期(生产过慢): 上游没水下来,发电机只能空转甚至停机等待(线程频繁阻塞唤醒,浪费 CPU 资源)
而缓冲区(阻塞队列),就是这座调节水位的"大坝水库"
它作为一个缓冲区,完美地解决了上下游速率不匹配的问题:
- 削峰: 当上游来水太急时,先把水蓄在水库里(入队),等下游慢慢处理。
- 填谷: 当上游没水时,释放库存的水让下游继续发电(出队),保证系统平稳运行。
这种机制不仅保护了下游的消费者不被冲垮,还实现了生产者和消费者的解耦------他们不需要知道对方的存在,只需要对着大坝操作即可。
那么,Java 是如何在底层实现这样一个精密的"数字大坝"的呢?其实,它的核心构造往往依赖于一种特殊的数组结构。这就不得不提我们之前深入探讨过的 轮盘数组,它是构建高效环形缓冲区的基石......
阻塞队列是一个以轮盘数组为骨架,以锁为灵魂的工具,接下来我们带着轮盘数组的思想来实现一个阻塞队列。
一、基础成员和构造方法
首先,我们实现基础成员和构造方法,我们以String类型为例
class MyBlockingQueue {
private String data[] = null;
public MyBlockingQueue(int n) {
this.data = new String[n];
}
private int size = 0;
private int head;
private int last;
}
由于接下来的put和take方法都会涉及到改,而改则会在多线程环境下产生线程安全问题,所以我们一定要记得加锁,这里我们以this作为锁
二、put(String elem)
public void put(String elem) throws InterruptedException {
//修改数组操作,多线程情况下必须加锁
synchronized (this){
while(size== data.length){
this.wait();
}
data[last] = elem;
size++;
last = (last+1)%data.length;
this.notify();
}
}
三、take()
public String take() throws InterruptedException {
synchronized (this){
while(size==0){
this.wait();
}
String ret = data[head];
size--;
head = (head+1)%data.length;
this.notify();
return ret;
}
}
⭐为什么我们不写成if而是要写成while循环判断队列的具体情况?
这里就要讲到为什么 wait()等待一定要配合while循环使用了:
·我们的wait等待不是一定只能有notify唤醒,还可能被Interrupt这样的方法给中断,如果使用if作为wait的判定条件,此时就存在wait被提前唤醒的风险,导致队列明明是(空/满),但我们还是执行了(put/take)方法,这会导致一些安全问题
⭐为了进一步解释为什么wait要配合while循环使用,接下来我将介绍一种极端情况:
我们有一个大小为3的阻塞队列,此时队列里已经有3个元素了,但此时有三个线程(t1,t2,t3)都调用了put方法,那么这三个线程一定都会阻塞,如果此时我们take()出了一个元素,那么take方法中的notify会随机唤醒一个正在因调用put方法且队列为满而阻塞的线程,我们设这个线程为t1,t1又put进一个元素,那么此时队列又为满了,但是t1调用的put方法中也有一个notify,那么这个notify很可能会去随机唤醒23线程中的随机一个,如果put方法中的while循环此时为if,那么就会唤醒if语句里的wait,线程继续往下执行,但别忘了,此时队列是满的,塞不进了,所以往下执行势必会产生一些安全问题
这样我们就成功实现了一个阻塞队列