本文简要介绍一下什么是阻塞队列,Java并发包给我们提供的阻塞队列有哪些,以及怎么去简单使用
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列:支持阻塞的插入和移除:
支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列变为不满
支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空
阻塞队列的使用场景: 常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、生产者用来获取元素的容器
BlockingQueue提供子类实现的几个API如下:
// 将指定元素插入队列,成功返回true;不成功返回false
boolean add(E e);
// 从队列中移出指定元素并返回该元素
boolean remove(Object o);
// 将指定元素插入队列,在使用有容量限制的队列时,该方法优于add()
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 获取队列的头元素并将其从队列中移除,如果队列为空返回null
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// 将指定的元素插入队列中
void put(E e) throws InterruptedException;
// 获取队列的头元素并将其从队列中移除
E take() throws InterruptedException;
阻塞队列的方法总结如下:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove(e) | poll() | take() | poll(time, unit) |
抛出异常:当队列满时往队列里面插入元素,会抛出 IllegalStateException 异常;当队列为空时从队列里面获取元素会抛出 NoSuchElementException 异常
返回特殊值:当往队列里插入元素时会返回元素是否插入成功,成功则返回true;移除方法,会获取队列的头元素并将其从队列中移除,如果队列为空返回null
一直阻塞:当队列满时,如果生产者线程往队列里面put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出;当队列为空时,如果消费者线程从队列里take元素,则队列会阻塞住消费者线程,直到队列不为空。
超时退出:当阻塞队列满时,如果生产者线程往队列里面插入元素,队列会阻塞生产者线程一段时间,超过指定时间后生产者线程将会退出
注意: 上面所说的队列满时插入元素的线程阻塞是针对有界队列的,如果是无界队列,队列不会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer()方法会永远返回true。
下图是BlockingQueue的继承体系:
ArrayBlockingQueue是一个由数组实现的有界阻塞队列。按照FIFO的原则对元素进行排序。队列使用可重入锁(ReentrantLock)对同步资源加锁,默认情况下不保证线程公平的访问队列(非公平锁)。下面是ArrayBlockingQueue源码中的几个成员变量:
// 保存队列元素的数组
final Object[] items;
// 将要取出元素的索引
int takeIndex;
// 将要添加元素的索引
int putIndex;
// 队列中已添加元素的数量
int count;
// 可重入锁
final ReentrantLock lock;
// 取元素线程等待队列:队列非空可获取
private final Condition notEmpty;
// 插入元素线程等待队列:队列非满可插入
private final Condition notFull;
如下是ArrayBlockingQueue的构造器源码:
// 传入一个容量
public ArrayBlockingQueue(int capacity) {
// 默认是非公平的
this(capacity, false);
}
// 可以传入一个boolean值,true是公平锁
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
分析ArrayBlockingQueue的put()和take()方法的源码:
// 插入元素
public void put(E e) throws InterruptedException {
// 检查掺入的元素是否为空,如果为空抛出NullPointerException异常
checkNotNull(e);
// 获取可重入锁
final ReentrantLock lock = this.lock;
// 可响应中断
lock.lockInterruptibly();
try {
// 如果队列已满,则掺入元素的线程等待,直到队列不满时由获取元素线程唤醒
while (count == items.length)
// 等待
notFull.await();
// 队列不满,插入元素
enqueue(e);
} finally {
lock.unlock();
}
}
// 向队列中插入元素
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒取元素线程,可以取元素了
notEmpty.signal();
}
// 获取元素
public E take() throws InterruptedException {
// 获得锁
final ReentrantLock lock = this.lock;
// 响应中断
lock.lockInterruptibly();
try {
// 当队列为空,获取元素的线程等待,直到队列不为空时由插入元素线程唤醒
while (count == 0)
// 等待
notEmpty.await();
// 返回取到的元素
return dequeue();
} finally {
lock.unlock();
}
}
// 从队列中获取元素
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
唤醒等待的插入元素线程,可以插入元素了
notFull.signal();
return x;
}
使用ArrayBlockingQueue实现一个简单的生产消费场景:
public class TestArrayBlockingQueue {
public static void main(String[] args) {
// 容量为3的阻塞队列
final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 两个生产者线程生产数据
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " 准备插入数据 " +
(queue.size() == 3 ? "...队列已满,正在等待..." : "..."));
queue.put(1);
System.out.println(Thread.currentThread().getName() + " 插入数据," +
"队列目前有 " + queue.size() + " 个数据.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
// 一个消费者线程消费数据
new Thread() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " 准备取出数据 " +
(queue.size() == 0 ? "...队列已空,正在等待..." : "..."));
queue.take();
System.out.println(Thread.currentThread().getName() + " 取出数据," +
"队列目前有 " + queue.size() + " 个数据.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
结果如下:
Thread-2 准备取出数据 ...队列已空,正在等待...
Thread-0 准备插入数据 ...
Thread-1 准备插入数据 ...
Thread-1 插入数据,队列目前有 2 个数据.
Thread-0 插入数据,队列目前有 2 个数据.
Thread-2 取出数据,队列目前有 1 个数据.
Thread-0 准备插入数据 ...
Thread-1 准备插入数据 ...
Thread-0 插入数据,队列目前有 2 个数据.
Thread-1 插入数据,队列目前有 3 个数据.
Thread-0 准备插入数据 ...队列已满,正在等待...
Thread-1 准备插入数据 ...队列已满,正在等待...
LinkedBlockingQueue 是一个由链表实现的有界阻塞队列。队列按照FIFO的原则对元素进行排序,队列默认的最大长度是:Integer.MAX_VALUE。该队列也是使用 ReentrantLock 对同步资源加锁。在线程池中,LinkedBlockingQueue是FixedThreadPool和SingleThreadExecutor的工作队列。下面是它的构造方法源码:
// 默认构造器
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 可设置容量的构造器
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingDeque 是一个由链表结构组成的双向有界阻塞队列:可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,减少了一半的竞争。该队列也是使用 ReentrantLock 对同步资源加锁。LinkedBlockingDeque 与其他阻塞队列相比,多了 addFist、addLast、offerFist、offerLast、peekFirst、peekLast等方法:以First结尾的方法表示插入、获取或移除双向队列的第一个元素;以Last结尾的方法表示插入、获取或移除双向队列的最后一个元素。下面是其构造器源码:
// 默认构造器
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
// 可设置容量的构造器
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列:默认情况下元素采取升序排列,也可以通过自定义类实现compareTo()方法来制定元素的排序规则,或者在初始化时制定构造参数Comparator来对元素进行排序。该队列也是使用 ReentrantLock 对同步资源加锁。下面是其构造器源码:
// 默认构造器
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 定制化排序
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
SynchronousQueue 是一个不存储元素的阻塞队列:每一个put操作必须等待一个take操作,否则不能继续添加元素。该队列也是使用 ReentrantLock 对同步资源加锁,默认情况下线程采用非公平策略访问队列。SynchronousQueue 的身份类似于一个中转者,负责把生产者线程处理的数据直接传递给消费者线程,队列本身并不存储任何元素,非常适合传递性场景。
LinkedTransferQueue 是一个由链表组成的无界阻塞队列。该队列也是使用 ReentrantLock 对同步资源加锁。相比于其他队列,该队列多了tryTransfer和transfer方法:
transfer方法:如果当前消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻传输给消费者。如果没有消费者等待接收元素,transfer方法会将元素存放在队列的tail节点(尾节点),并等到该元素被消费者消费了才返回。
tryTransfer方法:用来试探生产者传入的元素是否能直接传递给消费者,如果没有消费者等待接收,则返回false。与transfer方法的区别在于:无论消费者是否接收都会立即返回,而transfer方法是必须等到消费者消费了才返回。
DelayQueue 是一个支持支持延时获取元素的无界阻塞队列:队列使用PriorityQueue来实现。该队列也是使用 ReentrantLock 对同步资源加锁。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延时期满时才能从队列中提取元素。
DelayQueue的使用场景:
缓存系统的设计:使用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期已到。
定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行。
《Java并发编程的艺术》