BlockingQueue原理

概念

BlockingQueue 翻译成中文阻塞队列,顾名思义就是线程使用队列时会阻塞当前线程;

BlockingQueue 继承了Collection,具有一般集合所具有的数据存取功能

BlockingQueue 是线程安全的队列,多线程访问时不会出现同一个数据集中的数据被多次取出,或者覆盖存放的事件

 

使用场景

可用于一个快速反馈的消息队列,无消息时阻塞线程让出CPU,有数据存入时通知线程取出数据,取完后继续阻塞,

比如用户下单后立刻在大屏上显示有客户下单,比较简单的做法是开启一个定时任务,定期扫订单表;或者接入消息中间件,下单时发送消息,大屏服务监听消息;或者借用reddis队列 解决方式有很多种不一一列举

示例模拟数据的存取 设置队列的容量为1 是为了更好展示 存取的阻塞特性

public static void main(String[] args) throws InterruptedException {
    ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue(1);

    //模拟存入数据线程
    new Thread(()->{
        int i=0;
        while (true){
            try {
                //每次循环+1
                i++;
                queue.put(i);
                System.out.println("存入数据"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "存入数据线程").start();

    //模拟取出数据线程 1秒钟取一个
    new Thread(()->{
        while (true){
            try {
                //一秒钟取一个数据
                Thread.sleep(1000);
                Integer result = queue.take();
                System.out.println("取出数据"+result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "取数据线程").start();

}

打印结果:
存入数据1
取出数据1
存入数据2
取出数据2
存入数据3
取出数据3
存入数据4
取出数据4
存入数据5
取出数据5
存入数据6
取出数据6
存入数据7
取出数据7

 

方法示例

阻塞队列的使用非常简单,基本上和普通集合一样对数据进行存和取

public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue(2);

        //存入一个数据 如果队列满了则一直阻塞到有数据取出
        queue.put(1);
        //取出一个数据 如果队列空了则一直阻塞到有数据存入
        queue.take();

        //存入一个数据 如果队列满了则阻塞若干时长(示例为10秒),超时则返回offerResult=false
        boolean offerResult = queue.offer(1, 10, TimeUnit.SECONDS);
        //取出一个数据 如果队列空了则阻塞若干时长(示例为10秒),超时则返回pollResult=null
        Integer pollResult = queue.poll(10, TimeUnit.SECONDS);
}

 

源码分析

1、接口继承结构 

 

2、接口代码

public interface BlockingQueue<E> extends Queue<E> {
    //向队列中添加元素, 若超过给定队列长度抛出异常
    boolean add(E e);

    //向队列中添加元素, 若超过给定队列长度抛出异常
    boolean offer(E e);

    //向队列中添加元素, 若超过队列长度则等待队列有剩余容量再加入元素
    void put(E e) throws InterruptedException;

    //向队列中添加元素, 若超过给定队列长度则等待给定时长
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //获取队列头部元素,并从队列头部移除,若队列为空,则阻塞当前获取线程,并等待新元素加入
    E take() throws InterruptedException;

    //获取队列头部元素,并从队列头部移除,若队列为空,则阻塞当前获取线程,并等待元素给定时长
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    //返回队列剩余容量
    int remainingCapacity();

    //移除指定元素
    boolean remove(Object o);

    //返回是否存在指定元素
    public boolean contains(Object o);

    
    //将队列中的元素全部移除到给定的集合c中
    int drainTo(Collection<? super E> c);

    //将队列中的元素全部移除到给定的集合c中(最多不超过maxElements个)
    int drainTo(Collection<? super E> c, int maxElements);
}

 

3、实现类 ArrayBlockingQueue 分析

 

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    //数据集 用于存放元素 初始化固定数组长度 不再扩容
    final Object[] items;

    //数据集下一次取数据的下标
    //具体操作为 每次take加1 若take+1==items.length即take最后一个元素
    //则takeIndex重置为0 如此往复
    int takeIndex;

    //数据集下一次存数据下标
    int putIndex;

    //数据集中 存放元素的个数 即items[i]!=null的个数
    int count;

    //重入锁 可选公平与非公平 非本文重点
    final ReentrantLock lock;

    //Condition 
    //使用流程 1取数据为空(count==0) 则阻塞等待数据集存入数据 执行等待notEmpty.await 
    //         2存数据数据集肯定不为空(count!=0), 则通知取数据线程继续取数据 执行通知notEmpty.signal
    private final Condition notEmpty;

    //Condition 
    //使用流程 1存数据数据集存满(count==items.length)则等待消耗后重新存入 执行等待notFull.await 
    //         2取数据后则数据集未满肯定不满(count<items.length) 则通知存入数据 执行通知notFull.signal 
    private final Condition notFull;

    //用户维护ArrayBlockingQueue 作为集合的迭代(Iterator)功能
    //调用ArrayBlockingQueue.iterator()是初始化此属性 非本文重点
    transient Itrs itrs = null;



    //--------------------重点方法------------------------
    

    /**
     * 从队列中取一个元素
     * @return [description]
     * @throws InterruptedException [description]
     */
    public E take() throws InterruptedException {
        //对操作进行加锁 多线程时轮流取元素
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //如果队列中没有对象 则阻塞线程等待
            while (count == 0)
                //重点:等待存数据的线程通知
                notEmpty.await();
            //代码运行到此处说明count!=null 执行从队列中取元素
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        final Object[] items = this.items;
        //从数据集数组items 取出下标takeIndex的数据
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        //取完数据之后 将数组对应下标应用置为空(GC对象)
        items[takeIndex] = null;
        //takeIndex+1等于数组长度表示当前下标为数组最后一个对象
        //则takeIndex重新归0
        if (++takeIndex == items.length)
            takeIndex = 0;
        //每次取数据 数据总量减1
        count--;
        //迭代器维护操作
        if (itrs != null)
            itrs.elementDequeued();
        //重点:通知存数据的线程 可以执行数据存放
        notFull.signal();
        return x;
    }


    /**
     * 存入一个数据
     * @param  e                    [description]
     * @throws InterruptedException [description]
     */
    public void put(E e) throws InterruptedException {
        //校验数据非空
        checkNotNull(e);
        //加锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //若数据集数组items满了 则阻塞线程等待
            while (count == items.length)
                //重点:等待取出数据的线程通知
                notFull.await();
            //存入数据
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        final Object[] items = this.items;
        //存入数据到下标putIndex
        items[putIndex] = x;
        //如果存数据的下标已经到数据最后一个下标 则putIndex重新归0
        if (++putIndex == items.length)
            putIndex = 0;
        //数据总量加1
        count++;
        //重点:存入数据后通知等待取数据的线程
        notEmpty.signal();
    }




}

 

 总结:

BlockingQueue 重点关注

1、阻塞方式

Condition notFull 和 Condition notEmpty 的使用,存通知取,取通知存;

从而达到存满阻塞,取完阻塞,存入通知取,取出通知存的功能

2、存取游标

takeIndex 和 putIndex的使用,每次取数据takeIndex加1,到了数据末尾则重新回到数组开始下标0,存数据原理相似逐次加1,到末尾归0

对于LinkedBlockingQueue实现方式则略有不同,链表式集合多线程取数据时只需要排队从头部节点获取,从末尾存数据,有个小优化,创建LinkedBlockingQueue

时创建一个虚拟头部节点,不做深究

 

 

posted @ 2020-08-13 11:59  蟹烟客  阅读(2909)  评论(0编辑  收藏  举报