前言
在谈论阻塞队列之前我们先看下操作系统多线程部分一个经典的例子——生产者和消费者问题:
现在有两个进程,一个是生产者一个是消费者,还有一个线程缓冲区。生产者主要作用就是向缓冲区中添加数据,消费者就是从缓冲区中取出数据。这个问题的核心就是如何确保生产者不会在缓冲区满了的时候还往其中添加元素,消费者不会在缓冲区空了的时候还要求取出数据。
关于这个问题的解决办法我们以后再说,我们现在主要讨论线程缓冲区——阻塞队列。
阻塞队列简介
阻塞队列就是队列,只是在一般的队列上添加了两个条件:
- 当队列满了的时候不允许再添加数据
- 当队列空了的时候不允许从中取数据
在Java中,阻塞队列是通过BlockingQueue
来实现的,BlockingQueue
是Java.util.concurrent
包下一个重要的数据结构。
BlockingQueue的操作方法
方法 | 抛异常 | 返回特定值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(E e) | offer(E e) | put(E e) | offer(E e, long timeout, TimeUnit unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
解释:
- 抛异常:如果操作无法执行,则抛出一个异常
- 特定值:如果操作无法执行,则返回一个特定的值
- 阻塞: 如果操作无法执行,则方法调用被阻塞,直到可以执行
- 超时:如果操作无法执行,则方法调用被阻塞,直到可以执行或者超过限定的时间。返回一个特定值以告知该操作是否成功(典型的是true / false)。
Java中的各种阻塞队列
Java
基于BlockingQueue
给开发者提供了7个阻塞队列:
ArrayBlockingQueue
:基于数组的有界阻塞队列。有界就意味着他有一个最大限度,所存储的线程的数量不能超过这个限定值。你也可以在对其初始化的时候给定这个限定值。但是由于它是基于数组所以他和数组一样,在初始化的时候限定了这个大小以后就不能改变。LinkedBlockingQueue
:基于链表的阻塞队列。它内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用Integer.MAX_VALUE
作为上限。LinkedBlockingQueue
内部以FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。由于默认是无上限的,所以在使用他的时候,如果生产者的速度大于消费者的速度,系统内存可能会被耗尽。所以使用他一定要设置初值。PriorityBlockingQueue
:支持优先级的无界队列。默认情况按照自然顺序生序排列,你可以重写compateTo()
方法来制定元素按规定排序。DelayQueue
:支持延时获取元素的无界阻塞队列。队列中的元素必须实现Delayed
接口。SynchromousQueue
:是一个特殊的队列。他不能存储任何元素,他的每一次插入操作必须等待另一个线程相应的删除操作,反之亦然。LinkedTransferQueue
:基于链表的无界阻塞TransferQueue
队列。相对于其他队列,他多了transfer(E e)
、tryTransfer(E e)
和tryTransfer(E e, long timeout, TimeUnit unit)
方法。LinkedBlockingDeque
:是一个链表结构的双向阻塞队列。可在两端入队出对。所以当多线程入队时,减少了一半的竞争。
阻塞队列实现原理
下面我们以ArrayBlockingQueue
源码为例,来看下阻塞队列实现原理:
定义
首先就是一堆变量的定义:
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private
final Condition notEmpty;
/** Condition for waiting puts */
private
final Condition notFull;
items
是存储队列元素的数组,takeIndex
和putIndex
分别是取数据和存数据的索引,count
是队列中元素个数,lock
为看一个可重入锁,notEmpty
和notFull
均为等待条件,由loc
k创建。
构造器
接下来看下它的构造器
public ArrayBlockingQueue(int capacity) {
}
public ArrayBlockingQueue(int capacity, boolean fair) {
}
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {
}
构造器有三个重载的版本,第一个构造器只有一个参数用来指定容量,第二个构造器多了一个参数来指定访问策略,第三个构造器又多了一个参数可以指定用另外一个集合进行初始化。
数据的添加
接下来我们看看BlockingQueue
的三个插入的方法:put()
、add()
和offer()
:
put()
方法:队列满,会阻塞调用存储元素的线程
public void put(E e) throws InterruptedException {
// 先检查e是不是空,如果空则抛异常
Objects.requireNonNull(e);
// 获取一个重入锁lock
final ReentrantLock lock = this.lock;
// 加锁,保证调用put方法的时候只有1个线程
lock.lockInterruptibly();
try {
// 如果线程中的元素数量是否等于当前数组的长度,如果相等则调用await方法等待,如果不相等则enqueue方法插入元素
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
// 解锁
lock.unlock();