本文已参与「新人创作礼」活动,一起开启掘金创作之路。
小伙伴们大家好,这里是追風者。今天我们来聊一聊阻塞队列。
阻塞队列,是一种线程安全的、用于线程排队的一种队列。JUC 提供了其中阻塞队列,下面逐个分析。
ArrayBlockingQueue
数组阻塞队列,底层使用数组结构实现。ArrayBlockinhQueue 是有界阻塞队列,构造方法中对容量进行初始化。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
// 初始化数组,默认采用非公锁,初始化出两个条件队列
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 继承了 AbstractQueue 抽象类,AbstractQueue 采用的是模板方法模式,子类实现 Queue 接口,实现 Queue 中的方法来完成功能。
add() 方法
add() 方法在入队失败会抛出异常。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
// 插入 e
public boolean offer(E e) {
// 校验空
checkNotNull(e);
// 可重入锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 队列中元素与 数组长度相等时,入队失败
if (count == items.length)
return false;
else {
// 入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
// 将 x 加入到数组中。
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++;
// 可重入锁的 Conditon signal
// 唤醒 notEmpty 中的首节点进入到阻塞队列中,下次线程争抢锁时,就能执行了。
notEmpty.signal();
}
put() 方法
与 add() 方法比较,put() 方法是响应中断的,能够抛出中断异常,采用的锁也是可中断锁。并且,put() 方法在队列为满的时候,入队失败会阻塞线程。
// 将 e 加入到 数组队列中。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 响应中断的上锁
lock.lockInterruptibly();
try {
// 当 count 与数组长度相等,就进入条件队列
while (count == items.length)
// 将线程加入到条件队列中。阻塞线程
notFull.await();
// e 加入到数组中。
enqueue(e);
} finally {
lock.unlock();
}
}
// 将 x 加入到数组中。
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++;
// 可重入锁的 Conditon signal
// 唤醒 notEmpty 中的首节点进入到阻塞队列中,下次线程争抢锁时,就能执行了。
notEmpty.signal();
}
offer() 方法
由于 add() 使用的就是 offer() 方法,就不在这里赘述了。
出队操作
take() 方法
take() 方法是响应中断的,采用的是可中断锁。当队列为空时,线程会阻塞。
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")
// take 为出队索引
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
// itrs 迭代器不为空
if (itrs != null)
//
itrs.elementDequeued();
notFull.signal();
return x;
}
poll() 方法
与 tabke() 方法对比,poll() 在队列为空时,不会阻塞线程。
// 出队
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// dequeue() 出队
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
总结
我们可以看到,ArrayBlockingQueue 在插入到队列满状态时,要么是阻塞线程,要么直接抛出异常,所以它是有界队列。
| 操作 | 方法 | 特点 |
|---|---|---|
| 入队 | add() | 队列满会抛出异常 |
| 入队 | offer() | 入队会唤醒 notEmpty 条件队列,唤醒 notEmpty 队列 |
| 入队 | put() | 队列满会阻塞线程,将线程加入到 notFull 条件队列中 |
| 出队 | take() | 队列为空时,会将线程加入到 notEmpty 条件队列中;唤醒 notFull 队列 |
| 出队 | poll() | 队列为空时,直接返回空;唤醒 notFull 队列 |
通过上面的源码分析我们可以看出,add()、put() 底层都是调用的 offer(),区别在于调用后对结果的判定情况。take() 与 poll() 也是如此。
ArrayBlockingQueue 是通过一个 ReentrantLock 创建的两个条件队列,所以可以通过获取一个锁,对两个条件队列进行阻塞/唤醒来维护队列模型。
LinkedBlockingQueue
LinkedBlockingQueue 底层是单向链表。同样是有界队列。与 ArrayBlockingQueue 一样,继承了 AbstractQueue,采用其模板方法的方式实现功能。并且 linkedBlockingQueue 是链表结构,入队出队可以分离开,所以它实现了两个锁和两个队列,一个为入队锁、入队队列,一个为出对锁、出队队列。
// 默认的容量为最大值
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);
}
插入操作
由于与 ArrayBlockingQueue 类似,就直接放出 offer() 方法的代码。add() 方法通过模板方法调用 offer()。
// e 入队
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 当 元素数量与 阈值相等时,就返回 false 插入失败
if (count.get() == capacity)
return false;
int c = -1;
// 构建 e 的 Node
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
// 入队
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
// 入队操作
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
put() 方法入队:
// 响应中断
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
// 唤醒 noteEmpty 队列
signalNotEmpty();
}
// 由于两个锁分别创建的队列,此时就需要获取到出队锁。
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
出队操作
出队时,take() 与 poll() 的区别在于 take() 在队列空时能够阻塞线程。
总结
LinkedBlockingQueue 与 ArrayBlockingQueue 相比,方法的大体实现思路都是一致的,只不过由于链表的性质, LinkedBlockingQueue 维护了两个锁和两个条件队列。
LinkedBlockingDeque
LinkedBlockingDeque 底层是双向链表,它可以头查尾插、头删尾删。仍然采用一个锁两个同步队列的模式。锁是默认的非公平锁,通过 ConditionObject 来实现响应中断。
PriorityBlockingQueue
PriorityBlockingQueue 是一个无界队列,底层使用数组实现,当数组满的时候,它可以尝试进行扩容。默认的数组容量为 11。 PriorityBlockingQueue 的特点是,入队的数据必须要实现 compareTo 接口,否则运行时会抛出异常。队列会根据默认的比较器或者构造时传入的比较器进行排序,此排序在迭代器遍历时,不一定有序,但 poll()、take() 等方法会有序出队。
SynchronousQueue
SynchronousQueue 有两种模式,分别为 Stack 和 Queue。SynchronousQueue 插入元素后就会阻塞,直到有出队操作。 足以看出,SynchronousQueue 只能有一个元素在队列中。
DelayQueue
DelayQueue 是一个无界队列,内部维护了 PriorityQueue,PriorityQueue 就是非线程安全的 PriorityBlockingQueue。DelayQueue 主要是作为延时队列进行使用,入队的节点必须实现 Delayed 接口。DelayQueue 入队操作主要还是调用的 PriorityQueue 的 offer() 方法。DelayQueue 出队时,会对元素的延时进行校验 getDelay(NANOSECONDS) > 0,如果未到时间就阻塞条件队列或返回空值(看具体方法实现,poll() 会直接返回 null,而take() 会阻塞条件队列)。
LinkedTransferQueue
LinkedTransferQueue 为无界的阻塞队列。它的特点是 transfer() 方法和 tryTransfer() 方法。transfer() 方法可以直接把生产者的元素立即传输给消费者,如果没有消费者接收,transfer() 方法会将元素存放在队列的尾节点,等到该元素被消费才返回。tryTransfer() 是用来试探生产者传入的元素能够直接传给消费者。
注
阻塞队列的入队出队操作,节点都会延迟删除。