多线程编程-阻塞队列(BlockingQueue)
本文参考《Android进阶之光》
为什么使用阻塞队列?
-
更好的理解线程池
-
不用再关注线程的阻塞、同步、唤醒,阻塞队列帮我们完成,我们只需关注业务逻辑
阻塞队列简介
阻塞队列常用于生产者消费者的场景,生产者是往队列中添加元素的线程,消费者是从队列中拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
常见的阻塞常见
阻塞队列有两个常用的阻塞场景,分别是:
- 当队列没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到数据放入队列。
- 当队列填满数据,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
支持以上两种阻塞场景的队列被称为阻塞队列。
BlockingQueue核心方法
| 方法/处理方式 | 抛出异常 | 返回特殊 | 一直阻塞 | 超时退出 |
|---|---|---|---|---|
| 放入数据 | add(E e) | offer(E e)(true,flase) | put(E e) | offer(E e, long timeout, TimeUnit unit) |
| 获取数据 | remove(Object o) | poll()(null) | take() | poll(long timeout, TimeUnit unit) |
- 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalArgumentException异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常
- 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回
null。 - 一直阻塞:当阻塞队列满时,如果生产者线程往队列里
put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
Java中的阻塞队列
在Java中提供了7种阻塞队列,分别如下所示。
-
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
-
LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
-
DelayQueue:使用优先级队列实现的无界阻塞队列。
-
SynchronousQueue:不存储元素的阻塞队列。
-
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
-
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
主要学习一种就行,其余的只不过是实现方式不同,适用场景不同,了解一个的原理就差不离了。
ArrayBlockingQueue:
数组实现的有界阻塞队列,并按照先进先出的原则对元素进行排序。默认情况下不保证线程公平访问队列(按照线程阻塞的先后顺序进行访问队列)。
fair参数设置为true,则是公平的阻塞队列,默认为flase;通常情况下,为了保证公平性会降低吞吐量。
构造方法:
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();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c)
items[i++] = Objects.requireNonNull(e);
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
核心方法:
/**
* 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) {
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) {
Objects.requireNonNull(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 {
Objects.requireNonNull(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 {
Objects.requireNonNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0L)
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 <= 0L)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Removes a single instance of the specified element from this queue,
* if it is present. More formally, removes an element {@code e} such
* that {@code o.equals(e)}, if this queue contains one or more such
* elements.
* Returns {@code true} if this queue contained the specified element
* (or equivalently, if this queue changed as a result of the call).
*
* <p>Removal of interior elements in circular array based queues
* is an intrinsically slow and disruptive operation, so should
* be undertaken only in exceptional circumstances, ideally
* only when the queue is known not to be accessible by other
* threads.
*
* @param o element to be removed from this queue, if present
* @return {@code true} if this queue changed as a result of the call
*/
public boolean remove(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final Object[] items = this.items;
final int putIndex = this.putIndex;
int i = takeIndex;
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (++i == items.length) i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
LinkedBlockingQueue:
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
其中Integer.MAX_VALUE大小如下:
public static final int MAX_VALUE = 2147483647; // 0x7fffffff
所以注意,如果构造一个一个LinkedBlockingQueue对象,而没有指定其容量大小,当生产者速度远大于消费者的时候,可能没等到阻塞发生,内存就耗尽了。
这两个是最常用的阻塞队列,其余的有兴趣自己可以去了解一下。
阻塞队列的实现原理
以ArrayBlockingQueue为例,参考源码,查看实现。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final long serialVersionUID = -817911632652898426L;
final Object[] items;
int takeIndex;
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
ArrayBlocking维护的是一个Object的数组,takeIndex为队首下标,putIndex为队尾下标,count为队列元素个数,lock是个可重入锁,notEmpty和notFull是等待条件,下面通过核心方法分析。
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
put方法,先获取重入锁,并且获取的是可中断锁,然后判断当前队列元素个数是否等于数组的长度,如果相等,就notFull.await()等待条件。当此线程被其他线唤醒,通过enqueue(e)方法插入数据,最后解锁。然后查看下enqueue方法,如下所示:
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();
}
插入操作,通过notEmpty.signal()唤醒正在等待取元素的线程。再看take方法。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
如果队列元素为空,则进入等待,当被唤醒的时候,执行dequeue(),并接受返回。下面是dequeue()方法。
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;
}
获取元素,并且notFull.signal(),唤醒等会插入数据的线程,通知其当前队列已经不是满的了。
使用场景
除了线程池使用阻塞队列,还可以在生产者-消费者模式中使用阻塞队列:
首先使用Object.wait(),Object.notify()和非阻塞队列实现生产者-消费者模式,代码如下所示:
public class TestConsumerProduct {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
public static void main(String[] args) {
TestConsumerProduct testConsumerProduct = new TestConsumerProduct();
Product product = testConsumerProduct.new Product();
Consumer consumer = testConsumerProduct.new Consumer();
product.start();
consumer.start();
}
class Consumer extends Thread{
@Override
public void run() {
while(true){
synchronized(queue){
while(queue.size() == 0){
try{
System.out.println("队列空,等待数据");
queue.wait();
}catch(InterruptedException exception){
exception.printStackTrace();
queue.notify();
}
}
//每次移走队首元素
queue.poll();
queue.notify();
}
}
}
}
class Product extends Thread{
@Override
public void run() {
while(true){
synchronized(queue){
while(queue.size() == queueSize){
try{
System.out.println("队列满,等待有余空间");
queue.wait();
}catch(InterruptedException e){
e.printStackTrace();
queue.notify();
}
}
//每次插入数据
queue.offer(1);
queue.notify();
}
}
}
}
}
使用阻塞队列实现的生产者-消费者模式
public class TestForConsumerProducer {
private int queueSize = 10;
private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(queueSize);
public static void main(String[] args) {
TestForConsumerProducer testForConsumerProducer = new TestForConsumerProducer();
Product product = testForConsumerProducer.new Product();
Consumer consumer = testForConsumerProducer.new Consumer();
product.start();
consumer.start();
}
class Consumer extends Thread{
@Override
public void run() {
int i = 0;
while(++i < 10){
try{
queue.take();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
class Product extends Thread{
@Override
public void run() {
int i =0;
while(++i < 10){
try{
queue.put(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
阻塞队列实现无需单独考虑同步和线程间通信的问题,实现更简单。