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
时创建一个虚拟头部节点,不做深究