JAVA中常见的阻塞队列详解

- 在之前的线程池的介绍中我们看到了很多阻塞队列,这篇文章我们主要来说说阻塞队列的事。
- 阻塞队列也就是
BlockingQueue,这个类是一个接 - 口,同时继承了
Queue接口,这两个接口都是在JDK5中加入的 。 BlockingQueue阻塞队列是线程安全的,在我们业务中是会经常频繁使用到的,如典型的生产者消费的场景,生产者只需要向队列中添加,而消费者负责从队列中获取。

- 如上图展示,我们生产者线程不断的
put元素到队列,而消费者从中take出元素处理,这样实现了任务与执行任务类之间的解耦,任务都被放入到了阻塞队列中,这样生产者和消费者之间就不会直接相互访问实现了隔离提高了安全性。
并发队列

- 上面是
Java中队列Queue类的类图,我们可以看到它分为两大类,阻塞队列与非阻塞队列 - 阻塞队列的实现接口是
BlockingQueue而非阻塞队列的接口是ConcurrentLinkedQueue, 本文主要介绍阻塞队列,非阻塞队列不再过多阐述 BlockingQueue主要有下面六个实现类,分别是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue、LinkedTransferQueue。这些阻塞队列有着各自的特点和适用场景,后面详细介绍。- 非阻塞队列的典型例子如
ConcurrentLinkedQueue, 它不会阻塞线程,而是利用了CAS来保证线程的安全。 - 其实还有一个队列和
Queue关系很紧密,那就是Deque,这其实是double-ended-queue的缩写,意思是双端队列。它的特点是从头部和尾部都能添加和删除元素,而我们常见的普通队列Queue则是只能一端进一端出,即FIFO。
阻塞队列特点
- 阻塞队列的特点就在于阻塞,它可以阻塞线程,让生产者消费者得以平衡,阻塞队列中有两个关键方法
Put和Take方法
take方法
take方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除的。可是一旦执行take方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据。过程如图所示:

put方法
put方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到队列中。过程如图所示:

是否有界(容量有多大)
- 此外,阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。
- 无界队列意味着里面可以容纳非常多的元素,例如
LinkedBlockingQueue的上限是Integer.MAX_VALUE,约为 2 的 31 次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。 - 但是有的阻塞队列是有界的,例如
ArrayBlockingQueue如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
阻塞队列常见方法
- 首先我们从常用的方法出发,根据各自的特点我们可以大致分为三个大类,如下表所示:
| 分类 | 方法 | 含义 | 特点 |
|---|---|---|---|
| 抛出异常 | add | 添加一个元素 | 如果队列已满,添加则抛出 IllegalStateException 异常 |
| remove | 删除队列头节点 | 当队列为空后,删除则抛出 NoSuchElementException 异常 |
|
| element | 获取队列头元素 | 当队列为空时,则抛出 NoSuchElementException 异常 |
|
| 返回无异常 | offer | 添加一个元素 | 当队列已满,不会报异常,返回 false ,如果成功返回 true |
| poll | 获取队列头节点,并且删除它 | 当队列空时,返回 Null |
|
| peek | 单纯获取头节点 | 当队列为空时反馈 NULL |
|
| 阻塞 | put | 添加一个元素 | 如果队列已满则阻塞 |
| take | 返回并删除头元素 | 如果队列为空则阻塞 |
- 如上面所示主要的八个方法,相对都比较简单,下面我们通过实际代码演示的方式来认识
抛异常类型[add、remove、element]
add
- 向队列中添加一个元素。如果队列是有界队列,当队列已满时再添加则抛出异常提示,如下:
BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add(1);
queue.add(2);
queue.add(3);
- 上述代码中我们创建了一个阻塞队列容量为2,当我们使用
add向其中添加元素,当添加到第三个时则会抛出异常如下:

remove
remove方法是从队列中删除队列的头节点,同时会返回该元素。当队列中为空时执行remove方法时则会抛出异常,代码如下:
private static void groupRemove() {
BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add("i-code.online");
System.out.println(queue.remove());
System.out.println(queue.remove());
}
- 上述代码中,我们可以看到,我们想队列中添加了一个元素
i-code.online, 之后通过remove方法进行删除,当执行第二次remove时队列内已无元素,则抛出异常。如下:

element
element方法是获取队列的头元素,但是并不是删除该元素,这也是与remove的区别,当队列中没有元素后我们再执行element方法时则会抛出异常,代码如下:
private static void groupElement() {
BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add("i-code.online");
System.out.println(queue.element());
System.out.println(queue.element());
}
private static void groupElement2() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.element());
}
- 上面两个方法分别演示了在有元素和无元素的情况
element的使用。在第一个方法中并不会报错,因为首元素一直存在的,第二个方法中因为空的,所以抛出异常,如下结果:

无异常类型[offer、poll、peek]
offer
offer方法是向队列中添加元素, 同时反馈成功与失败,如果失败则返回false,当队列已满时继续添加则会失败,代码如下:
private static void groupOffer() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer("i-code.online"));
System.out.println(queue.offer("云栖简码"));
System.out.println(queue.offer("AnonyStar"));
}
- 如上述代码所示,我们向一个容量为2的队列中通过
offer添加元素,当添加第三个时,则会反馈false,如下结果:
true
true
false
poll
poll方法对应上面remove方法,两者的区别就在于是否会在无元素情况下抛出异常,poll方法在无元素时不会抛出异常而是返回null,如下代码:
private static void groupPoll() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer("云栖简码")); //添加元素
System.out.println(queue.poll()); //取出头元素并且删除
System.out.println(queue.poll());
}
- 上面代码中我们创建一个容量为2的队列,并添加一个元素,之后调用两次
poll方法来获取并删除头节点,发现第二次调用时为null,因为队列中已经为空了,如下:
true
云栖简码
null
peek
peek方法与前面的element方法是对应的 ,获取元素头节点但不删除,与其不同的在于peek方法在空队列下并不会抛出异常,而是返回null,如下:
private static void groupPeek() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer(1));
System.out.println(queue.peek());
System.out.println(queue.peek());
}
private static void groupPeek2() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.peek());
}
- 如上述代码所示,我么们分别展示了非空队列与空队列下
peek的使用,结果如下:

阻塞类型[put、take]
put
put方法是向队列中添加一个元素,这个方法是阻塞的,也就是说当队列已经满的情况下,再put元素时则会阻塞,直到队列中有空位.
take
take方法是从队列中获取头节点并且将其移除,这也是一个阻塞方法,当队列中已经没有元素时,take方法则会进入阻塞状态,直到队列中有新的元素进入。
常见的阻塞队列
ArrayBlockingQueue
ArrayBlockingQueue是一个我们常用的典型的有界队列,其内部的实现是基于数组来实现的,我们在创建时需要指定其长度,它的线程安全性由ReentrantLock来实现的。
public ArrayBlockingQueue(int capacity) {...}
public ArrayBlockingQueue(int capacity, boolean fair) {...}
- 如上所示,
ArrayBlockingQueue提供的构造函数中,我们需要指定队列的长度,同时我们也可以设置队列是都是公平的,当我们设置了容量后就不能再修改了,符合数组的特性,此队列按照先进先出(FIFO)的原则对元素进行排序。 - 和
ReentrantLock一样,如果ArrayBlockingQueue被设置为非公平的,那么就存在插队的可能;如果设置为公平的,那么等待了最长时间的线程会被优先处理,其他线程不允许插队,不过这样的公平策略同时会带来一定的性能损耗,因为非公平的吞吐量通常会高于公平的情况。
LinkedBlockingQueue
- 从它的名字我们可以知道,它是一个由链表实现的队列,这个队列的长度是
Integer.MAX_VALUE,这个值是非常大的,几乎无法达到,对此我们可以认为这个队列基本属于一个无界队列(也又认为是有界队列)。此队列按照先进先出的顺序进行排序。
SynchronousQueue
synchronousQueue是一个不存储任何元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。同时它也支持公平锁和非公平锁。synchronousQueue的容量并不是1,而是0。因为它本身不会持有任何元素,它是直接传递的,synchronousQueue会把元素从生产者直接传递给消费者,在这个过程中能够是不需要存储的- 在我们之前介绍过的线程池
CachedThreadPool就是利用了该队列。Executors.newCachedThreadPool(),因为这个线程池它的最大线程数是Integer.MAX_VALUE,它是更具需求来创建线程,所有的线程都是临时线程,使用完后空闲60秒则被回收,
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级排序的无界阻塞队列,可以通过自定义实现compareTo()方法来指定元素的排序规则,或者通过构造器参数Comparator来指定排序规则。但是需要注意插入队列的对象必须是可比较大小的,也就是Comparable的,否则会抛出ClassCastException异常。- 它的
take方法在队列为空的时候会阻塞,但是正因为它是无界队列,而且会自动扩容,所以它的队列永远不会满,所以它的put方法永远不会阻塞,添加操作始终都会成功
DelayQueue
DelayQueue是一个实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。DelayQueue应用场景:1. 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2. 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。- 它是无界队列,放入的元素必须实现
Delayed接口,而Delayed接口又继承了Comparable接口,所以自然就拥有了比较和排序的能力,代码如下:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
- 可以看出
Delayed接口继承Comparable,里面有一个需要实现的方法,就是getDelay。这里的getDelay方法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者负数则代表任务已过期。 - 元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。
本文由AnonyStar 发布,可转载但需声明原文出处。
欢迎关注微信公账号 :云栖简码 获取更多优质文章
更多文章关注笔者博客 :云栖简码 i-code.online
JAVA中常见的阻塞队列详解的更多相关文章
- Java 中的异常和处理详解
Java 中的异常和处理详解 原文出处: 代码钢琴家 简介 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常.异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误 ...
- Java中的多线程技术全面详解
本文主要从整体上介绍Java中的多线程技术,对于一些重要的基础概念会进行相对详细的介绍,若有叙述不清晰或是不正确的地方,希望大家指出,谢谢大家:) 为什么使用多线程 并发与并行 我们知道,在单核机器上 ...
- 关于Java中进程和线程的详解
一.进程:是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命 周期.它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而 ...
- java中ReentrantLock核心源码详解
ReentrantLock简介 ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活. ...
- JAVA中IO和NIO的详解分析,内容来自网络和自己总结
用一个例子来阐释: 一辆客车上有10个乘客,他们的目的地各不相同,当没有售票员的时候,司机就需要不断的询问每一站是否有乘客需要下车,需要则停下,不需要则继续开车,这种就是阻塞的方式. 当有售票员的时候 ...
- Java中23种经典设计模式详解
Java中23种设计模式目录1. 设计模式 31.1 创建型模式 41.1.1 工厂方法 41.1.2 抽象工厂 61.1.3 建造者模式 101.1.4 单态模式 131.1.5 原型模式 151. ...
- 2018.8.1 Java中的反射和同步详解
为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他 ...
- Java中的IO流系统详解(转载)
摘要: Java 流在处理上分为字符流和字节流.字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符.字符数组或字符串,而字节流处理单元为 1 个字节,操作字节和字节数组. Java ...
- Java中String 的equals 和==详解
一.Java中数据存储区域包括: 1.寄存器:最快的存储区,由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new ...
随机推荐
- iptables 和firewalld 区别
在RHEL7里有几种防火墙共存:firewalld.iptables.ebtables,默认是使用firewalld来管理netfilter子系统,不过底层调用的命令仍然是iptables等. fir ...
- 2440启动流程 <转载>
韦东山 博客园 首页 订阅 管理 2440启动过程分析 2440启动过程分析 2440启动过程算是一个难点,不太容易理解,而对于2440启动过程的理解,影响了后面裸机代码执行流程的分析,从而看出2 ...
- nginx优化:worker_processes/worker_connections/worker_rlimit_nofile
一,优化nginx的worker进程数 1,worker_processes应设置为多少? worker_processes 4; 如何设置这个值: worker_processes默认值是1,一般要 ...
- 圆形进度条的模仿3-DrawArc,DrawCircle,DrawText,自定义属性实例讲解
前面两篇中已经讲过如何使用drawARC,等,画其他的图形的方法的使用也是一样的,只是参数不同, 同时也讲了如何通过xml进行自定义属性,接下来这篇便是通过实例讲解如何实地应用起来, 效果如下,点击开 ...
- CSDN的Markdown编辑器实用技巧(傻瓜式教程)
markdown编辑器被很多人声称是可以取代word的文字编辑器,其优点我们在这就不再过多赘述了,但对于一些初次接触的人来说,或多或少都有还些不适应,其主要原因在于一些常见的功能突然不知道怎么实现,所 ...
- 【C++】 C++异常捕捉和处理
在阅读别人开发的项目中,也许你会经常看到了多处使用异常的代码,也许你也很少遇见使用异常处理的代码.那在什么时候该使用异常,又在什么时候不该使用异常呢?在学习完异常基本概念和语法之后,后面会有讲解. ( ...
- .gdbinit文件配置
.gdbinit文件配置 #打印数组的索引下标 set print array-indexes on #每行打印一个结构体成员 set print pretty on #除了断点有关的线程会被停下来, ...
- 今天 1024,为了不 996,Lombok 用起来以及避坑指南
Lombok简介.使用.工作原理.优缺点 Lombok 项目是一个 Java 库,它会自动插入编辑器和构建工具中,Lombok 提供了一组有用的注解,用来消除 Java 类中的大量样板代码. 目录 L ...
- 【Azure Developer】使用.Net Core解析JSON的笔记
在C#中解析JSON的一些历史代码记录,分别记录针对各种情况的解析方式. DLL的引用 using Newtonsoft.Json; using Newtonsoft.Json.Linq; 需要使用的 ...
- 《JavaScript高级程序设计》——第一章JavaScript简介
第一章主要讲了JavaScript的诞生和发展.刚刚接触JavaScript的我,似乎对这些内容并不感兴趣,快速看了一遍就开始去看第二章了. 看完第一章,收获也就是了解到JavaScript由ECMA ...