这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战
人生苦短,不如养狗
阻塞队列
上一次我们谈论了队列的基本原理和Java中的常见队列,今天我们来谈论一个较为特殊的队列——阻塞队列(BlockingQueue)。
基本概念
什么是阻塞队列?让我们来看看源码中对于阻塞队列的介绍:
* A {@link java.util.Queue} that additionally supports operations
* that wait for the queue to become non-empty when retrieving an
* element, and wait for space to become available in the queue when
* storing an element.
简单来说,阻塞队列就是能够支持等待队列不为空时在进行获取元素和当队列有空间时再进行存储的操作的一种队列。也就是说,获取元素时如果队列为空,则会阻塞取线程,等到队列不为空的时候在进行取数;而当存储元素时,如果队列满了,则阻塞写线程直到队列有空闲。
阻塞队列中的方法通过以下四种形式来处理那些没有办法立即满足,但在未来的某个时间点能够满足的操作:
- 直接抛出异常
- 返回一个特殊值(根据操作不同,返回
null
或者false
) - 阻塞当前的线程直到操作成功
- 设置一个最大阻塞时间,超时则放弃执行操作
需要注意一点,阻塞队列不支持null元素。所有操作遇到null元素时都会抛出NullPointerException
。
阻塞队列被设计主要用于生产者—消费者模型中,但是它也是集合中的一份子,尽管作为集合容器的时候效率没那么高。既然是属于集合的一份子,我们就需要考虑一个问题——线程安全。来看看源码中是怎么说的:
* <p>{@code BlockingQueue} implementations are thread-safe. All
* queuing methods achieve their effects atomically using internal
* locks or other forms of concurrency control. However, the
* <em>bulk</em> Collection operations {@code addAll},
* {@code containsAll}, {@code retainAll} and {@code removeAll} are
* <em>not</em> necessarily performed atomically unless specified
* otherwise in an implementation. So it is possible, for example, for
* {@code addAll(c)} to fail (throwing an exception) after adding
* only some of the elements in {@code c}.
可以看到所有的阻塞队列实现类都是线程安全的(thread-safe),所有的队列方法都原子性地使用了内在锁或者其他同步控制方式来保证线程安全。但是,请注意,对于大数据量的集合操作则没有必要使用原子性操作。
介绍完了BlockingQueue
的基本概念,我们来看一看BlockingQueue
接口到底长什么样?
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
boolean add(E e)
:当没有超过容量限制时能够立即插入,当达到容量限制时抛出IllegalStateException
boolean offer(E e)
:与add(E e)
方法不同,当队列满时则返回false
,当使用一个有容量限制的队列时,建议使用该方法而不是使用add(E e)
void put(E e)
:与add(E e)
方法不同,当队列满时会阻塞等到直到队列出现空闲boolean offer(E e, long timeout, TimeUnit unit)
:该方法可以灵活的设置等到超时时间,如果超过设置的超时时间,则放弃等待E take()
:获取并移除队首的元素,如果队列为空会等待直到队列不为空E poll(long timeout, TimeUnit unit)
:区别于E take()
,该方法可以设置等待超时时间,超过设置的时间则放弃等待int remainingCapacity()
:该方法用于返回不阻塞的情况下剩余的队列空间int drainTo(Collection<? super E> c)
:该方法是用于将队列中的元素全部转移至指定的容器中,但是当执行该方法的同时向目标集合中增加元素时会发生错误int drainTo(Collection<? super E> c, int maxElements)
:增加了maxElements
参数,用于指定迁移元素的数量
ArrayBlockingQueue
欣赏完了BlockingQueue
接口,下面我们以ArrayBlockingQueue
为例来看看实际中是如何实现一个阻塞队列的。
首先来看下ArrayBlockingQueue
中的成员变量
// 队列中的元素数组
final Object[] items;
// 用于标识下一个take, poll, peek或者remove的元素下标
int takeIndex;
// 用于标识下一个put, offer或者add的元素下标
int putIndex;
// 队列中元素的数量
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
// 用于保证所有准入的主锁
final ReentrantLock lock;
// 用于等待获取的Condition
private final Condition notEmpty;
// 用于等待放置的Condition
private final Condition notFull;
/**
* Shared state for currently active iterators, or null if there
* are known not to be any. Allows queue operations to update
* iterator state.
*/
transient Itrs itrs = null;
可以看到成员变量中有锁的存在,这也和上面说的所有的阻塞队列的实现类都是线程安全的相关。让我们来具体看一看ArrayBlockingQueue
中是如何实现线程安全的:
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and throwing an
* {@code IllegalStateException} if this queue is full.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws IllegalStateException if this queue is full
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
// 实际上内部使用了offer(e)方法
return super.add(e);
}
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full. This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
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();
}
}
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
/**
* Inserts the specified element at the tail of this queue, waiting
* up to the specified wait time for space to become available if
* the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
// 当队列中没有元素时
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
// 这里转换成毫微秒
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
// 可中断的加锁
lock.lockInterruptibly();
try {
while (count == 0) {
// 当队列中没有元素时
if (nanos <= 0)
return null;
// 这里按照设置的时间进行等待
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
可以看到所有的方法中都使用了Reentranlock
来保证线程安全。这也和上面源码中所说的虽然属于集合的一份子,但是效率上还是比较低的。对于ArrayBlockingQueue
中的其他方法这里就不一一介绍了,大家可以尝试自己阅读源码。
线程池中的阻塞队列
对于线程池,大家应该都不陌生。其中ThreadPoolExecutor
是《阿里巴巴代码规约》中建议使用的线程池。推荐的原因其实很简单,在使用ThreadPoolExecutor
时,需要自行对该线程池中的各个参数进行设置,如果不是对线程池的运行机制有所了解的话,可能就没有办法很好的运用ThreadPoolExecutor
。这样就从另一种程度上让开发人员去了解自己所使用的线程池到底是什么工作流程。当然直接使用Executor提供的四种线程池也有相应的缺点,这里就不展开了,大家可以自行Google。
ThreadPoolExecutor
的参数中有一个就是workQueue
,也就是阻塞队列。在ThreadPoolExecutor
提供了四种阻塞队列供大家使用:
- ArrayBlockingQueue 有界的阻塞队列,基于循环数组实现
- LinkedBlockingQueue 无界的阻塞队列(实际上是有界的,如果不设置大小,则队列的大小按照最大为Integer.MAX_VALUE),基于链表实现
- SynchronousQueue 内部没有存储容量,每个插入操作都必须同时有一个对应的删除操作,反之亦然
- PriorityBlockingQueue 具有优先级的阻塞队列
总结
以上就是对Java中的队列做的一点总结,当然本文和上一篇中介绍的队列基本以单向队列为主。在实际工作中,我们可能还会需要使用双向队列,那么就可从Deque
的实现类中寻找合适的双向队列。
相信大家在看完这两篇介绍队列的文章之后,应该对队列这一数据结构以及Java中实现的队列有了一些了解。后续闲鱼会针对队列的应用进行一些分享。