八种常用阻塞队列底层原理说明
(一)ArrayBlockingQueue介绍和实现原理分析
ArrayBlockingQueue是基于数组实现的有界的先进先出的阻塞队列,新来的元素都会被追加到队列的尾部,而出队操作是从队列的头部开始的。数组实现的阻塞队列可以充当典型的有界的buffer队列,因为其长度固定,一旦创建就不能变化,在队列满了或者空了,对应的生产者和消费者都会进入阻塞状态,此外数组队列还提供了可选的公平性策略,默认情况下是非公平,也就是说默认的访问是随机访问的,拥有更高的吞吐量,当设置成公平模式时,可以保证先进先出避免饥饿,但吞吐量会下降。
实现原理分析: ArrayBlockingQueue的内部采用了一个Object数组来保存元素,使用了ReentrantLock来保证同步,并通过重入锁的两个condition条件队列来分别控制生产者和消费者的阻塞和唤醒的调度通信,元素的插入和删除均是对数组的元素赋值,取走了就赋值null,其他就是数据本身,不像链表是按需所取。
(二)LinkedBlockingQueue介绍和实现原理分析
LinkedBlockingQueue是基于先进先出的阻塞队列,其容量可以有界的也可以是无界的,默认情况下是(Integer.MAX_VALUE.),也可以通过构造函数设置LinedBlockingQueue的容量大小。LinkedBlockingQueue相比ArrayBlockingQueue在大多数时候具有更高的吞吐量,但是由于链表的动态性所以其性能常常不稳定或者说难以预料。
实现原理分析:LinkedBlockingQueue采用了双锁队列,针对put和offer方法单独的使用一个锁,针对take和poll则采用了take锁,此外由于是两个锁,所以计数器count采用Atomic变量来更新,这样避免了同时操作2个锁来更新数据。这里面有个可见性的问题,因为2个锁是独立的,也就是put和take分别使用不同的同步块,那么put的数据在take里面如何使可见的?
在Java官网文档介绍,仅仅基于同一个监视器的锁,一个线程释放后另一个线程获得锁后才能得到可见性,但在这里却是利用volatile的增强语义来保证的可见性,put操作会更新使用volatile修饰的count变量,之后如果有读线程进入,如果先访问volatile修饰的count变量,那么volatile写对于读具有hanppend-before关系,也就是说只要访问了volatile变量,那么之前在不同锁的线程修改的数据会强制刷新到主cache里面,这样读线程就能够读取了,但这仅仅保证了可见性,对于原子性,是如何保证的呢? 这里恰恰是利用了队列的特点,队列的特点是头节点出队,末尾节点进队,也就是说任何时候不存在两个线程同时修改同一个节点从而巧妙的避免了该问题。
(三)PriorityBlockingQueue的介绍和实现原理分析
PriorityBlockingQueue可以根据自定义的优先级来构建一个有序的二叉堆数据结构,这种结构在插入数据的时候就能够根据自定义的排序规则(对象实现Compareable和Comparator)来生成一个有序的堆,通过这样来定义一个按优先级顺序的队列集合,不再是默认的先进先出规则,需要注意的是优先级队列的put方法并不阻塞,默认的数组的长度是11,在插入满的时候会扩容。take方法在队列为空的时候会进入阻塞状态。
实现原理分析:PriorityBlockingQueue使用一个ReentrantLock锁和一个控制消费者空的时候的condition条件队列,大多数操作都通过重入锁来保证互斥操作,唯一有一点特殊的地方在于,数组扩容的时候采用了自旋锁来控制,为了避免在扩容期间导致其他的并发操作不能进行。注意扩容是新生成一个容量更大的数组,等生成完毕之后,还是需要以独占锁的方法,先替换引用,然后在拷贝老数组的数据到扩容后的数组中。
(四)DelayQueue的介绍和实现原理分析
DelayQueue也是一个基于数组实现的阻塞队列,这个队列的功能可以说是PriorityBlockingQueue队列的加强版,首先其内部用的是PriorityQueue队列来存储相关的数据,这个优先级队列底层使用的也是二叉堆构建的数组数据结构,其中在DelayQueue的泛型中限制了其类必须是继承了Delayed这个类本身或者子类,在插入的时候一个有序的二叉堆便已经生成,与PriorityBlockingQueue不同的是,除了根据自定义的方法排序外,DelayQueue还支持延迟消费,也就说生产者创建的消息,在消费者消费的时候,并不说立刻就拿走了,还要判断延迟的时间是否到期,如果到期了才能消费,否则继续等待直到延迟的时间过时才能消费。
实现原理分析:这个类的大部分与PriorityBlockingQueue类似,不同点在于消费者消费数据的时候,会先通过peek方法取头部的元素出来,然后判断是否超时。如果没有超时,就调用Condition.awaitNanos(ns)方法阻塞到该数据超时时间,在此期间的其他消费者现场都必须阻塞等待,因为头部的元素如果还没超时,头部后面的元素就更加不会超时,因为该队列是排序过的。此外,该队列作为无界队列,插入方法也永远不会进入阻塞, 这个类也是使用的 ReentrantLock和条件量实现的同步策略。
(五)LinkedBlockingDeque的介绍和实现原理分析
LinkedBlockingDeque这个阻塞队列与LinkedBlockingQueue基本类似,两点区别如下:
(1)该阻塞队列是一个双向的链表结构,既然是双向,那么就意味着链表的两端都可以作为head,所以该类的api提供了特定add,put,take,peek,poll,remove,offer相关的xxxLast和xxxFirst方法,基于这些方法就能够从队列的两端进行操作。
(2)由于双向链表操作的复杂性,所以这个类的底层同步策略,并没有像LinkedBlockingQueue作双锁队列,仅仅用了一个ReentrantLock和两个条件队列来管理所有的访问操作,目的应该是简化实现,毕竟这个类的使用频次并不是很高。
(六)SynchronousQueue的介绍和实现原理分析
SynchronousQueue不存储实际的元素,仅仅是维护了两个线程队列,是一个生产者,一个消费者,采用类似CSP的模型,只要凑够一个生产者对一个消费者就立即执行,否则条件不满足就进入阻塞,消费者不关注消息是哪个生产者的。生产者也不关注哪个消费者取走了消息,这种模式在1对1的线程交换场景中效率比较高。
(七)LinkedTransferQueue的介绍和实现原理分析
LinkedTransferQueue是一个比较特殊的阻塞队列,其结合了SynchronousQueue和LinkedBlockingQueue优点,所以综合来说效率更高:
SynchronousQueue的优点在于1对于1的传递模型效率极高,但如果有大量数据时候,生产者和消费者的速率不均衡,那么性能就会大大下降,因为忙不过来的时候线程会阻塞。
LinkedBlockingQueue内部实现是通过加锁实现的,虽然已经在实现上有过优化,但整体来说表现一般。
LinkedTransferQueue同时兼具他们的优点,额外提供了如下几种方法:
transfer(E e):若当前存在一个正在等待获取的消费者线程,即立刻移交;否则,会插入到当前元素e到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素。
tryTransfer(E e):若当前存在一个正在等待获取的消费者线程(使用take()或者poll()函数),使用该方法会即刻转移/传输对象元素e;若不存在,则返回false,并且不进入队列。
tryTransfer(E e, long timeout, TimeUnit unit):若当前存在一个正在等待获取的消费者线程,会立即传输给它;否则将插入元素e到队列尾部,并且等待被消费者线程获取消费掉;若在指定的时间内元素e无法被消费者线程获取,则返回false,同时该元素被移除。
hasWaitingConsumer():判断是否存在消费者线程。
getWaitingConsumerCount():获取所有等待获取元素的消费线程数量。
LinkedTransferQueue在插入元素的时候可以优化成,如果当前已经有消费者在等待获取数据,那么生产者线程的数据则直接通过。transfer方法传递给该线程,避免了入队的开销,如果还可以采用异步的方法插入,tryTransfer方法会判断当前是否有消费者在等待获取数据,如果没有则数据入队,返回false;如果有则直接交换。最后还提供了可以指定一段超时的版本在一定时间内如果有消费者进入,那么就直接交换。
以ArrayBlockingQueue和LinkedBlockingQueue为例说明阻塞队列的构造与添加删除方法的实现(JDK1.8)。
(一)ArrayBlockingQueue
1、所具有的属性
final Object[] items; //队列的底层为数组,是个循环数组
int takeIndex; //从队列中取元素的索引,用于take、poll、remove
int putIndex; //向队列中存放元素的索引,用于put、offer、add
int count; //队列中的元素数
final ReentrantLock lock; //队列中的锁机制,可重入锁
private final Condition notEmpty; //notEmpty条件对象,由lock创建
private final Condition notFull; //notFull条件对象,由lock创建
transient Itrs itrs = null; //迭代器对象
2. ArrayBlockingQueue的添加方法
add(e)方法的底层实现:
/*调用了offer(e)方法,成功,返回true,失败,抛出IllegalStateException异常*/
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
add方法内部offer的实现
public boolean offer(E e) {
checkNotNull(e); //检查队列中的元素是否为空。在这里不允许为空
final ReentrantLock lock = this.lock; //引入重入锁
lock.lock(); //加锁,保证调用offer时只有一个线程
try {
if (count == items.length) //如果当前元素的个数等于队列数组的长度,说明队列是满的,添加失败
return false;
else {//否则队列不满,调用enqueue(e)方法添加元素,返回true
enqueue(e);
return true;
}
} finally {//最后,释放锁,让其他线程可以调用offer方法
lock.unlock();
}
}
enqueue(e)方法的实现:
/**
* 在当前put位置插入元素、前进和信号
* Call only when holding lock. 只有在持有锁资源时才调用该方法
*/
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) //如果将要插入的元素索引等于数组的长度,将存放元素的索引重新置为0
putIndex = 0;
count++;
notEmpty.signal(); //使用条件对象notEmpty通知,唤醒当前等待的线程
}
checkNotNull(Object obj)方法的实现:
/**
* Throws NullPointerException if argument is null.
*如果参数为null,则抛出NullPointerException的异常
* @param v the element
*/
private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
}
put方法的实现:
/**
* 将指定的元素插入到此队列的末尾,然后等待
* 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); //判断元素是否为null
final ReentrantLock lock = this.lock; //初始化重入锁
lock.lockInterruptibly(); //加锁,以保证在调用put方法时只有一个线程
try {
while (count == items.length) //当队列满了,阻塞当前线程,并加入到条件对象notFull的等待队列里面
notFull.await(); //线程阻塞并被挂起,同时释放锁资源
enqueue(e); //调用enqueue方法
} finally {
lock.unlock(); //释放锁,让其他线程可以调用put方法
}
}
ArrayBlockingQueue的添加数据方法有add,put,offer这3个方法,总结如下:
- add方法内部调用offer方法,如果队列满了,抛出IllegalStateException异常,否则返回true。
- offer方法如果队列满了,返回false,否则返回true。
- add方法和offer方法不会阻塞线程,put方法如果队列满了会阻塞线程,直到有线程消费了队列里的数据才有可能被唤醒。
这3个方法内部都会使用可重入锁保证原子性。
3. ArrayBlockingQueue的删除方法
poll()的实现: 从队列中移除一个元素,如果有,则返回移除的元素;如果没有,则返回null。
poll()方法的实现:
public E poll() {
final ReentrantLock lock = this.lock; //引入重用锁
lock.lock(); //加锁,以保证当前只有一个线程
try {//如果队列为空,则返回null;否则,调用dequeue方法
return (count == 0) ? null : dequeue();
} finally {
lock.unlock(); //释放锁资源,让其他线程可以调用poll方法
}
}
take()方法的实现:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//加锁,以保证在调用take()方法时只有一个线程
try {
while (count == 0) //当队列中元素个数为1,即队列为空时
notEmpty.await(); //阻塞当前线程,并加入到条件对象notEmpty的等待队列里
return dequeue(); //调用dequeue()方法
} finally {
lock.unlock(); //释放锁,让其他线程可以调用take()方法
}
}
remove(Object obj)方法的实现:
/* 从队列中删除指定的元素。如果该元素存在,则将该元素从队列中删除,返回true;如果不存在,则返回false
*/
public boolean remove(Object o) {
if (o == null) return false;//如果指定删除的元素为null,则返回false
final Object[] items = this.items; //阻塞队列数组
final ReentrantLock lock = this.lock; //重入锁
lock.lock(); //加锁,以此保证在调用该remove方法时只有一个线程
try {
if (count > 0) {//如果队列不为空
final int putIndex = this.putIndex; //往队列中即将要存储的元素的下标
int i = takeIndex; //从队列即将要取出元素的下标
//循环遍历阻塞队列中的元素,如果在队列中找到了要删除的元素,则将该元素删除,返回true;否则,返回false。
do {
if (o.equals(items[i])) { //
removeAt(i);
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);//结束条件为当前元素索引==最后将要存入队列中的元素的下标
}
return false;
} finally {
lock.unlock();//释放锁资源,让其他线程可以调用remove(e)方法
}
}
dequeue方法的实现:
/**
* Extracts element at current take position, advances, and signals.提取元素当前的位置、进展和信号
* Call only when holding lock.在持有锁时才调用
*/
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;//阻塞队列数组
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];//用变量x记录当前要取出的元素
items[takeIndex] = null;//将该元素置为null
if (++takeIndex == items.length)//判断是否是最后一个元素
takeIndex = 0; //如果是,将取元素索引置为0,从头开始取
count--;//元素个数-1
if (itrs != null) //迭代遍历队列,
itrs.elementDequeued();
notFull.signal();// 使用条件对象notFull通知,比如使用put方法放数据的时候队列已满,被阻塞。这个时候消费了一条数据,队列没满了,就需要调用signal进行通知
return x;
}
ArrayBlockingQueue的删除数据方法有poll,take,remove这3个方法,总结如下:
poll()方法:对于队列为空的情况,返回null,否则返回队列头部元素。
remove()方法:获取的元素是基于对象的下标值,删除成功返回true,否则返回false。
pull()方法和remove()方法不会阻塞线程。
take()方法:对于队列为空的情况,会阻塞并挂起当前线程,直到有数据加入到队列中。
(二)LinkedBlockingQueue
LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表。
内部使用放锁和拿锁,这两个锁实现阻塞(“two lock queue” algorithm)。
1、LinkedBlockingQueue带有的属性:
static class Node<E> {
E item; //元素
Node<E> next;//next指针
Node(E x) { //有参构造函数
item = x;
}
private final int capacity; //容量,默认为 Integer.MAX_VALUE
private final AtomicInteger count = new AtomicInteger(); //队列中元素的数量
transient Node<E> head; //头节点
private transient Node<E> last; //尾节点
private final ReentrantLock takeLock = new ReentrantLock(); //拿锁
private final Condition notEmpty = takeLock.newCondition(); //拿锁的条件,队列不为空
private final ReentrantLock putLock = new ReentrantLock(); //放锁
private final Condition notFull = putLock.newCondition(); //放锁的条件
}
LinkedBlocingQueue队列中有两把锁,拿锁和放锁。添加数据和删除数据可以并行进行,但同一时间只能有一个线程执行。
2、数据的添加
add()方法内部offer的实现原理:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();//判断要放入的元素是否为null,如果为null,抛出空指针异常(NullPointerException)
final AtomicInteger count = this.count; //队列中元素的个数
if (count.get() == capacity) //如果队列为满,返回false,结束
return false;
int c = -1; //用来记录元素个数
Node<E> node = new Node<E>(e); //如果队列不满,构造新的结点
final ReentrantLock putLock = this.putLock;//调用放锁
putLock.lock();//放锁加锁,以保证调用offer()方法的时候只有一个线程
try {
if (count.get() < capacity) {//再次判断队列是否满了,如果不满,继续执行
enqueue(node); //将结点添加到链表尾部
c = count.getAndIncrement(); //元素个数+1
if (c + 1 < capacity) //判断队列是否满了
notFull.signal(); //如果没有满,在放锁对象notFull上唤醒正在等待的线程
}
} finally {
putLock.unlock(); //释放放锁资源,让其他线程可以调用offer(e)方法
}
if (c == 0) // 由于存在放锁和拿锁,这里可能拿锁一直在消费数据,count会变化。这里的if条件表示如果队列中还有1条数据
signalNotEmpty(); //在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费
return c >= 0; //添加成功返回true,否则返回false
}
put(e)方法的实现:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();//判断添加的元素是否为null,如果为Null,抛出NullPointerException异常
int c = -1;
Node<E> node = new Node<E>(e); //构造新的结点
final ReentrantLock putLock = this.putLock; //放锁
final AtomicInteger count = this.count; //元素的个数
putLock.lockInterruptibly(); //放锁加锁,保证在调用put方法的时候只有1个线程
try {
while (count.get() == capacity) {//如果队列为满
notFull.await();//阻塞并挂起当前线程
}
enqueue(node);//将元素添加到链表的尾部
c = count.getAndIncrement(); //元素个数+1
if (c + 1 < capacity) //如果队列的容量还没有满
notFull.signal(); //在notFull对象上唤醒正在等待的1个线程,表示队列中还有元素可以消费
} finally {
putLock.unlock(); //释放放锁,让其他线程可以调用该put方法
}
if (c == 0)//由于存在放锁和拿锁,这里可能拿锁一直在消费数据,count会变化。这里的if条件表示如果队列中还有1条数据
signalNotEmpty();//在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费
}
enqueue(Node node)方法:
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
总结一下:
LinkedBlockingQueue添加数据的方法的add()、put()、offer()与ArrayBlockingQueue一样。不同的是它们的底层实现机制不同。ArrayBlocingQueue放入数据阻塞的时候,需要消费数据才能唤醒;而LinkedBlockingQueue放入数据阻塞的时候,因为它内部有两个锁,可以并行执行放入数据和消费数据,不仅能在消费数据的时候进行唤醒,插入阻塞的线程,同时在插入的时候如果容量还没满,也会唤醒插入阻塞的线程。
3、数据的删除
take()方法的实现:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count; //队列中元素的个数
final ReentrantLock takeLock = this.takeLock; //拿锁
takeLock.lockInterruptibly(); //拿锁加锁,以保证在调用take()方法的时候只有一个线程
try {
while (count.get() == 0) { //如果队列为空
notEmpty.await(); //则将当前线程阻塞并挂起
}
x = dequeue(); //否则,删除头节点
c = count.getAndDecrement(); //元素个数-1
if (c > 1) //判断队列中是否还有元素
notEmpty.signal(); //如果有,在拿锁的条件对象notEmpty上唤醒正在等待的线程,表示队列里还有数据,可以再次消费
} finally {
takeLock.unlock(); //释放拿锁,以保证其他线程可以调用take()方法
}
if (c == capacity) //表示如果队列中还可以再插入数据
signalNotFull(); //在放锁的条件对象notFull上唤醒正在等待的1个线程,表示队列里还能再次添加数据
return x; //返回删除的那个元素
}
poll()方法的实现:
public E poll() {
final AtomicInteger count = this.count; //队列中元素的个数
if (count.get() == 0) //判断该队列是否为空
return null; //如果为空,返回null
E x = null; //定义要返回的元素的变量名,初始化为Null
int c = -1;
final ReentrantLock takeLock = this.takeLock;//拿锁
takeLock.lock();//拿锁加锁,以保证在调用poll()线程的时候只有1个线程
try {
if (count.get() > 0) {//判断队列是否为空。如果不为空
x = dequeue();//删除头节点
c = count.getAndDecrement();//元素个数-1
if (c > 1)//如果队列中还有元素
notEmpty.signal();//在拿锁的条件对象notEmpty上唤醒正在等待的线程,表示队列里还有数据,可以再次消费
}
} finally {
takeLock.unlock();//释放拿锁资源,让其他线程可以调用该poll()方法
}
if (c == capacity)//由于存在放锁和拿锁,这里可能放锁一直在添加数据,count会变化。这里的if条件表示如果队列中还可以再插入数据
signalNotFull();//在放锁的条件对象notFull上唤醒正在等待的1个线程,表示队列里还能再次添加数据
return x;//返回删除的元素
}
remove()方法的实现:
public boolean remove(Object o) {
if (o == null) return false; //如果要删除的元素为null,返回false
fullyLock(); //remove操作要移动的位置不固定,2个锁都需要加锁
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {//判断在队列中是否能找到要删除的对象
unlink(p, trail);//修改节点的链接信息,同时调用notFull的signal方法 ,唤醒等待的线程
return true;
}
}
return false;//如果没有找到,返回false
} finally {
fullyUnlock();//2个锁解锁
}
}
remove()方法中的加锁方法(fullyLock):
void fullyLock() {
putLock.lock();
takeLock.lock();
}
remove()方法中的解锁方法():
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
删除头节点的方法(dequeue):
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
总结一下:
LinkedBlockingQueue的take方法对于没数据的情况下会阻塞,poll方法删除链表头结点,remove方法删除指定的对象。