1. BlockingQueue 简介
在实际编程中,会经常使用到 JDK 中 Collection 集合框架中的各种容器类如实现 List,Map,Queue 接口的容器类,但是这些容器类基本上不是线程安全的,除了使用 Collections 可以将其转换为线程安全的容器,Doug Lea 大师为我们都准备了对应的线程安全的容器,如实现 List 接口的 CopyOnWriteArrayList(关于 CopyOnWriteArrayList 可以看这篇文章),实现 Map 接口的 ConcurrentHashMap(关于 ConcurrentHashMap 可以看这篇文章),实现 Queue 接口的 ConcurrentLinkedQueue(关于 ConcurrentLinkedQueue 可以看这篇文章)。
最常用的”生产者-消费者“问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
2. 基本操作
BlockingQueue 基本操作总结如下(此图来源于 JAVA API 文档):
| Throws exception | Special value | Blocks | Times out | |
|---|---|---|---|---|
| Insert | add(e) | offser(e) | put(e) | offser(e,time,unit) |
| Remove | remove() | poll() | take() | poll(time,unit) |
| Examine | element() | peek() | - | - |
BlockingQueue 继承于 Queue 接口,因此,对数据元素的基本操作有:
插入元素
add(E e):往队列插入数据,当队列满时,插入元素时会抛出IllegalStateException异常;offer(E e):当往队列插入数据时,插入成功返回true,否则则返回false。当队列满时不会抛出异常;
删除元素
remove(Object o):从队列中删除数据,成功则返回true,否则为falsepoll():删除数据,当队列为空时,返回 null;
查看元素
element():获取队头元素,如果队列为空时则抛出NoSuchElementException异常;peek():获取队头元素,如果队列为空则抛出NoSuchElementException异常
BlockingQueue 具有的特殊操作:
插入数据:
put(E e):当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用;offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与put方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出;
删除数据
take():当阻塞队列为空时,获取队头数据的线程会被阻塞;poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出
3. 常用的 BlockingQueue
实现 BlockingQueue 接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明:
1.
ArrayBlockingQueue
**ArrayBlockingQueue**是由数组实现的有界阻塞队列。该队列命令元素 FIFO(先进先出)。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。ArrayBlockingQueue 可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue 一旦创建,容量不能改变。
当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。
ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,一旦 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:
1 | private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true); |
关于 ArrayBlockingQueue 的实现原理,可以看这篇文章。
2.
LinkedBlockingQueue
LinkedBlockingQueue 是用链表实现的有界阻塞队列,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE
3.
PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。
4.
SynchronousQueue
SynchronousQueue 每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue 实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue 也可以通过构造器参数来为其指定公平性。
5.
LinkedTransferQueue
LinkedTransferQueue 是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了 TransferQueue 接口,与其他阻塞队列相比主要有以下不同的方法:
transfer(E e) 如果当前有线程(消费者)正在调用 take()方法或者可延时的 poll()方法进行消费数据时,生产者线程可以调用 transfer 方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出;
tryTransfer(E e) tryTransfer 方法如果当前有消费者线程(调用 take 方法或者具有超时特性的 poll 方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与 transfer 方法相比,transfer 方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而 tryTransfer 方法能够立即返回结果退出。
tryTransfer(E e,long timeout,imeUnit unit)
与 transfer 基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false。
6.
LinkedBlockingDeque
LinkedBlockingDeque 是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为 Integer.MAX_VALUE。与 LinkedBlockingQueue 相比,主要的不同点在于,LinkedBlockingDeque 具有双端队列的特性。LinkedBlockingDeque 基本操作如下图所示(来源于 java 文档)

LinkedBlockingDeque的基本操作.png
如上图所示,LinkedBlockingDeque 的基本操作可以分为四种类型:
1.特殊情况,抛出异常;
2.特殊情况,返回特殊值如 null 或者 false;
3.当线程不满足操作条件时,线程会被阻塞直至条件满足;
4. 操作具有超时特性。
另外,LinkedBlockingDeque 实现了 BlockingDueue 接口
而 LinkedBlockingQueue 实现的是 BlockingQueue,
这两个接口的主要区别如下图所示(来源于 java 文档):

BlockingQueue和BlockingDeque的区别
从上图可以看出,两个接口的功能是可以等价使用的,比如 BlockingQueue 的 add 方法和 BlockingDeque 的 addLast 方法的功能是一样的。
7.
DelayQueue
DelayQueue 是一个存放实现 Delayed 接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过 poll 等方法获取数据元素则返回 null。所谓数据延时期满时,则是通过 Delayed 接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于 0 则说明该数据元素的延时期已满。
ArrayBlockingQueue 实现原理
阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)。那么,多线程中消息通知机制最常用的是 lock 的 condition 机制,关于 condition 可以看这篇文章的详细介绍。那么 ArrayBlockingQueue 的实现是不是也会采用 Condition 的通知机制呢?下面来看看。
2.1 ArrayBlockingQueue 的主要属性
ArrayBlockingQueue 的主要属性如下:
1 | /** The queued items */ |
从源码中可以看出 ArrayBlockingQueue 内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是 Condition,当获取数据的消费者线程被阻塞时会将该线程放置到 notEmpty 等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到 notFull 等待队列中。而 notEmpty 和 notFull 等中要属性在构造方法中进行创建:
1 | public ArrayBlockingQueue(int capacity, boolean fair) { |
接下来,主要看看可阻塞式的 put 和 take 方法是怎样实现的。
2.2 put 方法详解
put(E e)方法源码如下:
1 | public void put(E e) throws InterruptedException { |
该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到 notFull 等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。enqueue 方法源码为:
1 | private void enqueue(E x) { |
enqueue 方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。
2.3 take 方法详解
take 方法源码如下:
1 | public E take() throws InterruptedException { |
take 方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作dequeue。dequeue 方法源码为:
1 | private E dequeue() { |
dequeue 方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知 notFull 等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得 lock,并执行完成功退出。
从以上分析,可以看出 put 和 take 方法主要是通过 condition 的通知机制来完成可阻塞式的插入数据和获取数据。在理解 ArrayBlockingQueue 后再去理解 LinkedBlockingQueue 就很容易了。
3. LinkedBlockingQueue 实现原理
LinkedBlockingQueue 是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出:
1 | public LinkedBlockingQueue() { |
3.1 LinkedBlockingQueue 的主要属性
LinkedBlockingQueue 的主要属性有:
1 | /** Current number of elements */ |
可以看出与 ArrayBlockingQueue 主要的区别是,LinkedBlockingQueue 在插入数据和删除数据时分别是由两个不同的 lock(takeLock和putLock)来控制线程安全的,因此,也由这两个 lock 生成了两个对应的 condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node 结点的定义为:
1 | static class Node<E> { |
接下来,我们也同样来看看 put 方法和 take 方法的实现。
3.2 put 方法详解
put 方法源码为:
1 | public void put(E e) throws InterruptedException { |
put 方法的逻辑也同样很容易理解,可见注释。基本上和 ArrayBlockingQueue 的 put 方法一样。take 方法的源码如下:
1 | public E take() throws InterruptedException { |
take 方法的主要逻辑请见于注释,也很容易理解。
4. ArrayBlockingQueue 与 LinkedBlockingQueue 的比较
相同点:ArrayBlockingQueue 和 LinkedBlockingQueue 都是通过 condition 通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;
不同点:1. ArrayBlockingQueue 底层是采用的数组进行实现,而 LinkedBlockingQueue 则是采用链表数据结构;
- ArrayBlockingQueue 插入和删除数据,只采用了一个 lock,而 LinkedBlockingQueue 则是在插入和删除分别采用了
putLock和takeLock,这样可以降低线程由于线程无法获取到 lock 而进入 WAITING 状态的可能性,从而提高了线程并发执行的效率。