一、简介
BlockingQueue是juc包下提供的一种队列工具,被称作阻塞队列,他的工作模式类似于生产者和消费者的工作过程。在juc的线程池中使用到,做为存储任务的队列。内部使用了Lock锁和Condition条件参数作为阻塞功能实现的基础,可以在文章《JUC-Lock工具解析》中简单了解Lock的实现方式,从而能更高的理解BlockingQueue的实现原理。
二、使用
BlockingQueue是一个很经典的生产者-消费者类型的队列模式,生产者不断的向队列中生产输入,如果队列满了则会阻塞生产者直到队列不满;另一方面消费者则不断的从队列中获取消费,如果队列是空的,则会阻塞消费者,直到队列不为空。下面是一个ArrayBlockingQueue使用的简单的例子,可以大致的了解一下他的工作方式:
public class BlockingQueueTest {
public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(512);
Producer producer = new Producer(queue);
Consumer consumer1 = new Consumer(queue);
Consumer consumer2 = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer1).start();
new Thread(consumer2).start();
}
}
class Producer implements Runnable {
BlockingQueue queue;
@Override
public void run() {
while (true) {
try {
queue.put(produce());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Producer(BlockingQueue q) {
this.queue = q;
}
Object produce() {
Object o = new Object();
System.out.println("producer is produce " + " " + o.hashCode());
return o;
}
}
class Consumer implements Runnable {
BlockingQueue queue;
@Override
public void run() {
while (true) {
try {
consume(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Consumer(BlockingQueue q) {
this.queue = q;
}
void consume(Object o) {
System.out.println("consumer is consume " + " " + o.hashCode());
}
}
在官方的介绍上,也有一个关于BlockIngQueue的使用实例,他使用的是伪代码的方式,是以BlockingQueue接口为题展示的,更能显示BlockingQueue的通用特性。
三、解析
BlockingQueue是一个接口,他继承于集合类,因此具备集合类的操作属性,接口继承关系如下图所示:
BlockingQueue作为接口形式存在,定义了一系列操作队列的方法,全部方法如下图所示:
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);
方法性质:简单的描述这些方法的作用方式以及特点,如下图所示(部分方法是集合类下的操作方式)
操作类别 | 抛异常 | 特殊值 | 阻塞 | 超时 |
添加 | add() | offer() | put() | offer(time) |
获取 | remove() | poll() | take() | poll(time) |
校验 | element() | peek() | 不支持 | 不支持 |
1.抛异常
当操作没有得到立即执行的时候,抛出异常;
2.特殊值
当操作没有得到立即执行的时候,返还特定的值(比如true/false);
3.阻塞
当操作没有得到立即执行的时候,此方法会发生阻塞,直到满足条件能够执行;
4.超时
当操作没有得到立即执行的时候,此方法会发生阻塞,等待时间不会超出传入的规定时间,返回特定值(比如true/false);
队列特点:
BlockIngQueue:禁止使用null值,队列添加元素会对null值进行判断;
BlockIngQueue:无界和有界,比如数组队列需要指定队列大小,链表队列则可以不指定(Integer最大值);
BlockIngQueue:可以使用集合相关操作,继承于集合。比如remove()、forEach()等;
BlockIngQueue:是线程安全的,使用Lock锁;
主要实现:
ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue以及接口DelayQueue以及它的实现类LinkedBlockingDeque等。
队列 | 队列特性 |
ArrayBlockingQueue(数组队列) | 数组实现,有界队列,FIFO |
LinkedBlockingQueue(链表队列) | 链表实现,有界队列或无界(Integer最大值) |
PriorityBlockingQueue(优先级队列) | heap结构,默认按照自然排序,可以指定排序方式 |
SynchronousQueue(单元素队列) | 队列中最多只能有一个元素 |
ArrayBlockingQueue讲解:
下面我们看下ArrayBlockingQueue的实现逻辑,首先我们看下此类的成员变量工具及其相应的作用
//底层数组
final Object[] items;
//下一次take,poll,peek,remove方法操作的数组元素下标
int takeIndex;
//下一次put,offer,add方法操作的数组元素下标
int putIndex;
//队列元素的数量
int count;
//队列操作的锁
final ReentrantLock lock;
//消费队列的Condition
private final Condition notEmpty;
//生产队列的Condition
private final Condition notFull;
//迭代器
transient Itrs itrs = null;
生产队列:
接下来我们看下入队的操作add()方法逻辑,源码如下:
public boolean add(E e) {
return super.add(e);//调用父类的add()方法
}
可以看到调用的是抽象父类AbstractQueue的add()逻辑如下 :
public boolean add(E e) {
if (offer(e))//Queue接口定义的方法
return true;
else
throw new IllegalStateException("Queue full");//队列满,则抛出异常
}
父类的add()将会调用offer()方法,如果成功,则返回true;如果失败,则说明队列已经满了,直接抛出异常。队列工具ArrayBlockingQueue实现了此offer()方法,逻辑如下:
public boolean offer(E e) {
checkNotNull(e);//校验null
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
if (count == items.length)//如果队列满了,返回false
return false;
else {
enqueue(e);//如果队列不满,则执行入队方法,并返回true
return true;
}
} finally {
lock.unlock();
}
}
首先会有一个Lock加锁操作,然后判断队列是否是满的状态,根据维护的一个队列元素数量值和队列数组的length去比较,如果队列满了即二者相等,则返回fasle,add()方法会抛出异常;如果不满,则执行入队操作,进入方法enqueue()方法:
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;//添加元素至入队元素下标处
if (++putIndex == items.length)//如果队列满了,则将putIndex值设为0,重复使用
putIndex = 0;
count++;//维护队列元素值加一
notEmpty.signal();//通知消费者们,队列有元素
}
下面再看下提供阻塞功能的put()方法逻辑:
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();
}
}
可以看到和offer()方法的逻辑基本上是相同的,不同之处在于对于队列满了之后的处理逻辑,offer()是直接返回false,而put()方法则是调用了await()方法,使得本线程阻塞在此处,并且释放了锁,其他线程也会阻塞在这个地方,等待消费队列的线程消费队列,唤醒阻塞的生产线程。
消费队列:
下面我们看下消费队列的方法,和生产队列的方法一样,是区分是否阻塞的,因为两种模式相差不多,所以放在一起查看,方便对比,逻辑如下:
//非阻塞
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();//队列空,则返回null
} 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();
}
}
可以看到和生产队列的阻塞方式是一样的,通过调用Condition的await()方法。当队列空的时候就会对本线程进行阻塞,如果队列不为空的话,则会进入dequeue()方法,出队一个元素,逻辑如下:
private E dequeue() {
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;
}
取出数组takeIndex下标处的值,然后释放此下标元素,看下出队小标位置是否等于数组长度,如果等于,则将takeIndex设为0,最后通知生产者,队列不满了,可以生产了。
总结:
1.通过构造函数创建ArrayBlockingQueue实例,指定队列的size;
2.生产者线程向队列中添加元素,如果队列元素数量已经达到指定size大小,则根据方法区分处理(异常、标识、阻塞);
3.消费者去队列中消费弹出元素,如果队列中无元素可消费,则根据方法区分处理(标识、阻塞);
以上是ArrayBlockingQueue的简单的源码简析,其他阻塞队列可以自行看下源码,尝试解析他们的工作过程。
四、资源地址
文档:《Thinking in java》
jdk1.8版本源码
BlockingQueue官方文档:https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html