文章目录
一.什么是阻塞队列?
来自AI解释:阻塞队列是一种特殊的队列,它支持两个附加操作:
- 在队列为空时,获取元素的线程会等待队列变为非空;
- 当队列满时,存储元素的线程会等待队列可用。
- 这种特性使得阻塞队列在生产者和消费者的场景中非常有用,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
阻塞队列的特性包括:
- 阻塞功能:当队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用。
- 线程安全:阻塞队列是一种能保证线程安全的数据结构。
- 阻塞队列常用于生产者和消费者的场景,例如在多线程环境中,生产者生产数据放入队列,消费者从队列中取出数据进行处理。这种机制可以平衡生产者和消费者的速度,防止生产过快导致资源浪费或消费者处理不过来。
说人话:
队列满了阻塞生产者直到队列中有位置可存放。队列都满了,生产者当然要等待队列还有位置给放再生产(不然生产出来的放哪去对吧),等消费者消费空出一个位置了就可以通知生产者“我消费了一个,你有位置放东西了,快点继续干活了”。
队列空了阻塞消费者直到队列中有元素可消费。队列里面都没有东西消费了,消费者当然需要阻塞等待啦,等生产者生产元素之后就可以通知消费者“我这又生产了一个,你别等着了,可以消费啦”。
其实综上描述,我们也可以猜到就是用到我们的多线程等待唤醒机制了。
二.创建阻塞队列
1.参照ArrayBlockingQueue设计,我们照着它思路来分析。
/**
- 数组阻塞队列:
*/
@Slf4j
class MyBlockingQueue{
// 数组
private Object [] items;
// 记录队列里面还剩的元素数量
private int size = 0;
// 记录取元素的索引、 存元素的索引
int takeIndex,putIndex;
// 存取元素都公用一把互斥锁、保证多线程存取队列安全
private ReentrantLock lock;
/**
* 消费者线程阻塞唤醒条件:队列为空阻塞,生产者生成完成唤醒
*/
public Condition customer_Condition;
/**
* 生产者线程阻塞唤醒条件:队列满了阻塞,消费者消费完唤醒
*/
public Condition producer_Condition;
public MyBlockingQueue(int capacity) {
this.items = new Object[capacity];
lock = new ReentrantLock();
customer_Condition = lock.newCondition();
producer_Condition = lock.newCondition();
}
//生产者存元素:
public void put(Object value) throws Exception{
lock.lock();
try {
//队列已满,生产者阻塞等待:
while(size == items.length){
log.info("队列已满,生产者阻塞...");
producer_Condition.await();
}
items[putIndex] = value;
log.info("生产者生产的元素:{},生产元素位置:{}",value,putIndex);
//循环队列取元素生产:如果生产元素的位置到了数组末尾,则又从数组0号索引位置开始存放
if (++putIndex == items.length){
putIndex = 0;
}
// 生产完,数组里面的元素数量+1
size++;
//生产完成唤醒消费者:
customer_Condition.signal();
}finally {
lock.unlock();
}
}
//消费者取元素:
public Object take() throws Exception {
lock.lock();
try {
//队列为空,消费者阻塞:
if(size == 0){
customer_Condition.await();
}
Object item = items[takeIndex];
log.info("消费者消费的元素:{},消费元素位置:{}",item,takeIndex);
//消费完了,位置元素置为空
items[takeIndex] = null;
//循环队列取元素消费:如果消费的位置到了数组末尾,则又从数组0号索引位置开始消费
if(++takeIndex == items.length){
takeIndex = 0;
}
// 消费完,数组里面的元素数量-1
size--;
//唤醒生产者生产:
producer_Condition.signal();
return item;
}finally {
lock.unlock();
}
}
}
三.开始愉快的测试啦
1. 生产速度 > 消费速度 场景
1.1 生产者:1s生产一次,消费者:2s消费一次
/**
* 生产者:1s生产一次
*/
class Producer implements Runnable{
private MyBlockingQueue queue;
public Producer(MyBlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try{
// 每隔1秒钟轮询生产一次
while(true){
Thread.sleep(1000);
queue.put(new Random().nextInt(1000));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 消费者:2s消费一次
*/
class Customer implements Runnable{
private MyBlockingQueue queue;
public Customer(MyBlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try{
// 每隔2秒钟轮询消费一次
while(true){
Thread.sleep(2000);
System.out.println("Customer消费信息:"+ queue.take());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
1.2 测试方法
图方便先直接到main方法测试了…
public static void main(String[] args) {
//创建队列长度为5的队列
MyBlockingQueue queue = new MyBlockingQueue(5);
//启动生产者线程
new Thread(new Producer(queue),"生产者线程").start();
//启动消费者线程
new Thread(new Customer(queue),"消费者线程").start();
}
1.3 预测结果
我们这里是生产速度明显大于消费速度的场景,我们猜下上面可能出现的场景:
生产2个,消费一个
生产2个,消费一个
生产1个,消费一个
…
最终
生产者阻塞
消费者消费一个
生产者生产1个
生产者阻塞
消费者消费一个 等等这样无限的生产阻塞生产阻塞状态。 ok.预想是这样
1.4 测试结果
跟我们预想差不多,最终陷入一个无限的生产阻塞生产阻塞状态。
2. 消费速度 > 生产速度 场景
2.1 生产者:1s生产一次,消费者:2s消费一次
/**
* 生产者:1s生产一次
*/
class Producer implements Runnable{
private MyBlockingQueue queue;
public Producer(MyBlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try{
// 每隔1秒钟轮询生产一次
while(true){
Thread.sleep(2000);
queue.put(new Random().nextInt(1000));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 消费者:2s消费一次
*/
class Customer implements Runnable{
private MyBlockingQueue queue;
public Customer(MyBlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try{
// 每隔2秒钟轮询消费一次
while(true){
Thread.sleep(1000);
System.out.println("Customer消费信息:"+ queue.take());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
2.2 测试结果
- 可以看到刚生产完就立马被消费完了,无限进入到生产->消费->生产->消费循环中。很健康的生产消费方式
四.小结
- 好啦,先介绍这么多,相信小伙伴们对阻塞队列的设计思想有了初步的简单了解。
- 其实就是使用同一把lock锁管控读取+2个Condition条件(是否满了 / 是否空了)来实现多线程的等待唤醒机制
- 前提条件,一定要先加锁再存取元素,避免多线程操作队列,造成线程不安全。
- 然后就是使用Condition条件来实现逻辑: 队列满了阻塞生产者生产,等待消费者消费,直到有位置可生产。队列空了阻塞消费者消费,等待生产者生产,直到有元素可消费
补充:多线程还有一种是等待唤醒机制是通过synchronized+wait+notify来实现的。
1.wait()、notify()和notifyAll()一般是跟synchronized配合一起使用,这些方法都是Object类提供的。
2.Condition类的await()、signal()和signalAll(),一般是配合Lock锁一起使用,是显式的线程间协调同步操作类。
我这里只简单介绍了下数组阻塞队列(ArrayBlockingQueue)的设计思想,像阻塞队列还有很多种,比如LinkedBlockingQueue、延迟队列DelayQueue等等,感兴趣的小伙伴们可以去了解下~