【浅浅聊一下阻塞队列, 工作中感觉有同事对这块懵懵的】

一.什么是阻塞队列?

来自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等等,感兴趣的小伙伴们可以去了解下~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值