一、阻塞队列


【1】首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:

当阻塞队列为空时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞。
【2】在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
【3】为什么需要 BlockingQueue:好处在于我们不需要关心什么时候阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了。
【4】在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这个细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂性。

二、架构分析


【1】阻塞队列的架构图:阻塞队列与 List 具有很多类似之处,对比着学习会更加容易一些。

【2】阻塞队列重点子类说明: 
   ■  ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
   ■  LinkedBlockingQueue:由链表结构组成的有界(大小默认值为 Integer.MAX_VALUE <21亿左右,相当于无界>)阻塞队列。
   ■  PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
   ■  DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
   ■  SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
   ■  LinkedTransferQueue:由链表结构组成的无界阻塞队列。
   ■  LinkedBlockingQeque:有链表组成的双向阻塞队列。

三、BlockingQueue 的核心方法


【1】 抛出异常:当阻塞队列满时,再往队列中 add 插入元素会抛出 IllegalStateException:Queuefull。当队列为空时,再从队列中 remove 移除元素会抛出 NoSuchElementException。实例如下:

  1. public class BlockingQueueDemo {
  2. public static void main(String[] args) {
  3. //必须制定默认的大小,arrayList 不用指定是因为默认值为 10
  4. ArrayBlockingQueue<String> blocking = new ArrayBlockingQueue<>(2);
  5. //正常插入2个
  6. blocking.add("a");
  7. blocking.add("b");
  8. //add 第三个是出现如下错误 java.lang.IllegalStateException: Queue full
  9. //blocking.add("c");
  10.  
  11. //该方法会获取阻塞队列中,将要出队列的值。这里的 a
  12. blocking.element();
  13.  
  14. //正常删除插入的两个值
  15. blocking.remove();
  16. blocking.remove();
  17. //当阻塞队列中为空时,使用remove,出现如下错误 java.util.NoSuchElementException
  18. //blocking.remove();
  19.  
  20. //如果为空值,则抛出 java.util.NoSuchElementException
  21. blocking.element();
  22. }
  23. }

【2】特殊值:使用插入方法 offer() 向阻塞队列中插入值时,阻塞队列未满,插入成功后返回 true。如果阻塞队列已满,则插入失败返回 false。使用移除方法 poll(),如果阻塞队列中有值,则移除成功返回队列的元素第一个元素,如果队列为空则返回 null 。

  1. public class BlockingQueueDemo {
  2. public static void main(String[] args) {
  3. //必须制定默认的大小,arrayList 不用指定是因为默认值为 10
  4. ArrayBlockingQueue<String> blocking = new ArrayBlockingQueue<>(2);
  5. //正常插入2个返回 true
  6. System.out.println(blocking.offer("a"));
  7. System.out.println(blocking.offer("b"));
  8. //offer 第三个是返回 false
  9. System.out.println(blocking.offer("c"));
  10.  
  11. //该方法会获取阻塞队列中,将要出队列的值。这里的 a
  12. System.out.println(blocking.peek());
  13.  
  14. //正常取出插入的两个值 a b
  15. System.out.println(blocking.poll());
  16. System.out.println(blocking.poll());
  17. //当阻塞队列中为空时,poll 的到 null
  18. System.out.println(blocking.poll());
  19.  
  20. //如果为空值,则为 null
  21. System.out.println(blocking.peek());
  22. }
  23. }

【3】一直阻塞:当阻塞队列满时,生产者继续向队列中 put 元素,队列会一直阻塞生产线程直到 put 数据或者响应中断。当阻塞队列为空时,消费者线程试图从队列中 take 元素,队列会一直阻塞消费者线程直到队列可用。

  1. 1 public class BlockingQueueDemo1 {
  2. 2 public static void main(String[] args) throws InterruptedException {
  3. 3 //必须制定默认的大小,arrayList 不用指定是因为默认值为 10
  4. 4 ArrayBlockingQueue<String> blocking = new ArrayBlockingQueue<>(2);
  5. 5 //正常插入2个返回 true
  6. 6 blocking.put("a");
  7. 7 blocking.put("b");
  8. 8 //put 第三时,线程会被阻塞,直到有人消费掉阻塞队列中的元素。
  9. 9 blocking.put("c");
  10. 10
  11. 11 //正常取出插入的两个值 a b
  12. 12 blocking.take();
  13. 13 blocking.take();
  14. 14 //当阻塞队列中为空时,take 会阻塞线程,直到队列中有元素
  15. 15 blocking.take();
  16. 16 }
  17. 17 }

【4】超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,超过指定时间生产者线程会退出,并返回 false。当阻塞队列为空时,通过 poll 指定获取时间,超过时间后,消费者线程会退出,并返回 null。

  1. 1 public class BlockingQueueDemo1 {
  2. 2 public static void main(String[] args) throws InterruptedException {
  3. 3 //必须制定默认的大小,arrayList 不用指定是因为默认值为 10
  4. 4 ArrayBlockingQueue<String> blocking = new ArrayBlockingQueue<>(2);
  5. 5 //正常插入2个返回 true
  6. 6 blocking.offer("a");
  7. 7 blocking.offer("b");
  8. 8 //put 第三时,线程会被阻塞,直到有人消费掉阻塞队列中的元素。
  9. 9 System.out.println("开始第3 个元素插入==开始");
  10. 10 blocking.offer("c",3, TimeUnit.SECONDS);
  11. 11 System.out.println("开始第3 个元素插入==结束");
  12. 12
  13. 13 //正常取出插入的两个值 a b
  14. 14 blocking.poll();
  15. 15 blocking.poll();
  16. 16 //当阻塞队列中为空时,take 会阻塞线程,直到队列中有元素
  17. 17 System.out.println("开始第3 个元素获取==开始");
  18. 18 blocking.poll(3,TimeUnit.SECONDS);
  19. 19 System.out.println("开始第3 个元素获取==结束");
  20. 20 }
  21. 21 }

四、SynchronousQueue


SynchronousQueue 没有容量,与其他 BlockingQueue 不同,SynchronousQueue  是一个不存储元素的阻塞队列。每一个put 操作必须要等待一个 take 操作,否则不能续集添加元素,反之亦然。

  1. 1 public class SynchronousQueueDemo {
  2. 2 public static void main(String[] args) {
  3. 3 SynchronousQueue<String> queue = new SynchronousQueue<>();
  4. 4 //创建两个线程对数据进行操作
  5. 5 //生产者线程,生产三个数据
  6. 6 new Thread(()->{
  7. 7 try {
  8. 8 System.out.println("插入第一个数据 A 线程名:"+Thread.currentThread().getName());
  9. 9 queue.put("a");
  10. 10 System.out.println("插入第二个数据 B 线程名:"+Thread.currentThread().getName());
  11. 11 queue.put("b");
  12. 12 System.out.println("插入第二个数据 C 线程名:"+Thread.currentThread().getName());
  13. 13 queue.put("c");
  14. 14 } catch (InterruptedException e) {
  15. 15 e.printStackTrace();
  16. 16 }
  17. 17 },String.valueOf("Product")).start();
  18. 18
  19. 19 //消费者线程,消费三个数据
  20. 20 new Thread(()->{
  21. 21 try {
  22. 22 TimeUnit.SECONDS.sleep(3);
  23. 23 System.out.println("3秒后消费第一个数据 A 线程名:"+Thread.currentThread().getName());
  24. 24 queue.take();
  25. 25 TimeUnit.SECONDS.sleep(3);
  26. 26 System.out.println("3秒后消费第二个数据 B 线程名:"+Thread.currentThread().getName());
  27. 27 queue.take();
  28. 28 TimeUnit.SECONDS.sleep(3);
  29. 29 System.out.println("3秒后消费第二个数据 C 线程名:"+Thread.currentThread().getName());
  30. 30 queue.take();
  31. 31 } catch (InterruptedException e) {
  32. 32 e.printStackTrace();
  33. 33 }
  34. 34 },String.valueOf("Consumer")).start();
  35. 35 }
  36. 36 }

输出结果展示:
 

五、ArrayBlockingQueue 与 LinkedBlockingQueue 区别


1底层实现:ArrayBlockingQueue :底层基于数组实现,在对象创建时需要指定数组大小。在构建对象时,已经创建了数组。所以使用Array需要特别注意设定合适的队列大小,如果设置过大会造成内存浪费。如果设置内存太小,就会影响并发的性能。功能上,其内部维护了两个索引指针 putIndextakeIndexputIndex表示下次调用 offer时存放元素的位置,takeIndex表示的时下次调用take时获取的元素。有了这两个索引的支持后,还是无法说明白其底层的实现原理。那么我们来看一段其内部出现最多的代码:

  1. int i = takeIndex;
  2. ...
  3. if (++i == items.length)
  4. i = 0;
  5. ...

这几行在代码在 Array中几乎每个函数都会用到。意思不管是在读取元素,或者存放元素,如果到达数组的最后一个元素,直接将索引移动到第一个位置。你可能会想,如果我一直往队列中添加元素而不取,添加的元素个数超过了数组长度,会不会覆盖之前添加的元素。在实际使用过程中是不会出现这种情况的,其内部使用了ReentrantLockCondition[链接],这部分在并发支持中介绍。
LinkedBlockingQueue:底层基于单向链表实现。实现了队列的功能,元素到来放到链表头,从链表尾部取数据。这种数据结构没有必要使用双向链表。链表的好处(数组的没有的)是不用提前分配内存。Link也支持在创建对象时指定队列长度,如果没有指定,默认为Integer.MAX_VALUE
【2】并发支持:最大的区别就是 Array内部只有一把锁,offer 和 take使用同一把锁,而 Link的 offer和 take使用不同的锁。

ReentrantLock 和 Condition的关系:ReentrantLock内部维护了一个双向链表,链表上的每个节点都会保存一个线程,锁在双向链表的头部自选,取出线程执行。而 Condition内部同样维持着一个双向链表,但是向链表中添加元素(await)和从链表中移除(signal)元素没有像 ReentrantLock那样,保证线程安全,所以在调用 Condition的 await()和 signal()方法时,需要在 lock.lock()lock.unlock()之间以保证线程的安全。在调用 Condition的 signal时,它从自己的双向链表中取出一个节点放到了 ReentrantLock的双向链表中,所以在具体的运行过程中不管 ReentrantLock new 了几个 Condition其实内部公用的一把锁。介绍完这个之后,我么来分析 ArrayBlockingQueue和 LinkedBlockingQueue的内部实现不同。

ArrayBlockingQueue:先看其内部锁的定义:

  1. int count;
  2. lock = new ReentrantLock(fair);
  3. notEmpty = lock.newCondition();
  4. notFull = lock.newCondition();

lock 是其内部锁,当调用阻塞队列的 offer时,会调用 notEmpty.signal() 通知之前因为队列空而被阻塞的线程。同时在 take后,如果内部计数器 count=0时,会调用 notEmpty.await() 阻塞调用 take的线程。当调用阻塞队列的 offer时,如果现在 count=内部数组的长度时,会调用 notFull.await()阻塞现在添加元素的所有线程;当调用 take时,总会调用 notFull.signal()唤醒之前因为队列满而阻塞的线程。根据上面分析 ReentrantLock和其 Condition的关系,可以看到放元素和取元素用的同一把锁,无法使放元素和取元素同时进行,只能先后相继执行。

LinkedBlockingQueue:内部锁定义:

  1. /** Current number of elements */
  2. private final AtomicInteger count = new AtomicInteger();
  3.  
  4. /** Lock held by take, poll, etc */
  5. private final ReentrantLock takeLock = new ReentrantLock();
  6.  
  7. /** Wait queue for waiting takes */
  8. private final Condition notEmpty = takeLock.newCondition();
  9.  
  10. /** Lock held by put, offer, etc */
  11. private final ReentrantLock putLock = new ReentrantLock();
  12.  
  13. /** Wait queue for waiting puts */
  14. private final Condition notFull = putLock.newCondition();

count 内部元素计数器使用的原子类型的计数器,使的元素个数的更新支持并发,为下面取和放元素并发提供了支持。takeLock 取元素单独的锁,和放元素分开,这样即使有 Condition也可以使的取和放元素在不同的节点上自选。notEmpty 取元素的Condition锁,和放元素锁分开。putLock notFull 和上面介绍的 takeLock notEmpty一致。通过这种设置,可以将在链表头上放元素和在链表尾部取元素不再竞争锁,在一定程度上可以加快数据处理

为什么要这么设计呢?因为 ArrayBlockingQueue使用更简单的数据结构来保存队列项。ArrayBlockingQueue将其数据存储在一个私有的final E []items;array中。对于多个线程来处理相同的存储空间,无论是添加还是出列,它们都必须使用相同的锁。这不仅是因为内存障碍,还因为互斥体保护,因为它们正在修改同一个数组。
另一方面,LinkedBlockingQueue是一个队列元素的链接列表,它完全不同,允许有双重锁。队列中元素的内部存储启用了不同的锁配置。
我觉得还是因为数组的入队和出队时间复杂度低,不像列表需要额外维护节点对象。所以当入队和出队并发执行时,阻塞时间很短。如果使用双锁的话,会带来额外的设计复杂性,如 count应被 volatile修饰,并且赋值需要 CAS操作等。而且ArrayBlockingQueue是定长的,当putIndex==length时,putIndex会重置为0,这样入队和出队的 index可能是同一个,在这种情况下还需要考虑锁之间的通讯,参考读写锁。

六、应用场景


【1】生产者消费者:链接
【2】线程池:链接
【3】消息中间件

----关注公众号,获取更多内容----

Java面试——阻塞队列的更多相关文章

  1. java面试-阻塞队列

    一.阻塞队列 当阻塞队列是空,从队列中获取元素的操作会被阻塞 当阻塞队列是满,往队列中添加元素的操作会被阻塞 二.为什么用,有什么好处? 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为 ...

  2. Java多线程 阻塞队列和并发集合

    转载:大关的博客 Java多线程 阻塞队列和并发集合 本章主要探讨在多线程程序中与集合相关的内容.在多线程程序中,如果使用普通集合往往会造成数据错误,甚至造成程序崩溃.Java为多线程专门提供了特有的 ...

  3. Java集合--阻塞队列及各种实现的解析

    阻塞队列(Blocking Queue) 一.队列的定义 说的阻塞队列,就先了解下什么是队列,队列也是一种特殊的线性表结构,在线性表的基础上加了一条限制:那就是一端入队列,一端出队列,且需要遵循FIF ...

  4. Java:阻塞队列

    Java:阻塞队列 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 1. 概述 概念 队列 队列就可以想成是一个数组,从一头进入,一头出去,排队买饭 阻塞队列 B ...

  5. JAVA可阻塞队列-ArrayBlockingQueue

    在前面的的文章,写了一个带有缓冲区的队列,是用JAVA的Lock下的Condition实现的,但是JAVA类中提供了这项功能,就是ArrayBlockingQueue, ArrayBlockingQu ...

  6. java 可伸缩阻塞队列实现

    最近一年多写的最虐心的代码.必须好好复习java并发了.搞了一晚上终于测试都跑通过了,特此纪念,以资鼓励! import java.util.ArrayList; import java.util.L ...

  7. java 多线程阻塞队列 与 阻塞方法与和非阻塞方法

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  8. Java -- 使用阻塞队列(BlockingQueue)控制线程通信

    BlockingQueeu接口是Queue的子接口,但是它的主要作用并不是作为容器,而是作为线程同步的工具. 特征: 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程 ...

  9. Java并发--阻塞队列

    在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList),这些工具都为我们编写多线程程 ...

  10. Java中阻塞队列的使用

    http://blog.csdn.net/qq_35101189/article/details/56008342 在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如 ...

随机推荐

  1. SQL----EXISTS 关键字EXISTS基本意思

    1.EXISTS基本意思 英语解释就是存在,不过他的意思也差不多,相当于存在量词'З'.他不返回数据的,当后带带的查询为空值是,返回"FALSE",非空则返回"TRUE& ...

  2. Win10系统删除文件需提供管理员权限-- 解决方案

    解决方案1:选中[文件]-[属性]-[安全]-[高级]-选中当前用户[编辑]权限 若还是不行,则试试方案2解决方案2:更改[所有者]--[高级]--[立即查找] 选中[everyone]--[确定] ...

  3. 项目实训 day15-16

    第一天我与灿哲沟通,我弄明白了真正的网络结构且如何运行的,自己记了下网络草图,开始初步用PlotNN绘制 第二天我发现pycore库表达能力不够,于是参考其他用tex写的例子,写了几个方法,最终能生成 ...

  4. 修改tomcat启动时,修改默认访问的页面

  5. seleniumUI自动化学习记录

    2019.2.9 尝试了一个启动浏览器并打开指定网址的程序: 这里首先要注意的就是浏览器的版本和selenium jar包的版本必须符合才行,不然会报错 2019.9.16 必须要下载相应的chrom ...

  6. angular-gridster2使用

    1.安装:npm install angular-gridster2 --save 2.引入 3.html代码 <div id="fullscreen" style=&quo ...

  7. 阿里云Linux服务器部署JDK8实战教程

    下载地址 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 文件上传 把下载的文 ...

  8. springboot整合mybatis:查询语句,返回null

    springboot整合mybatis时,查询数据库数据时,返回结果为null; 刚开始以为是数据库没连接上,结果增.改.删的其他语句则执行成功: 但唯有查询语句始终返回null,一条数据一个null ...

  9. 20181224蒋嘉豪-exp4

    20181224蒋嘉豪-exp4 目录 20181224蒋嘉豪-exp4 实验概况 1.实践目标 2.实践内容概述 知识点总结 1.有关schtasks 2.有关Sysmon(参考链接) 3.恶意代码 ...

  10. numpy基本使用(一)

    一.简介  NumPy(Numerical Python) 是用于科学计算及数据处理的Python扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库. 二.数据结构  n ...