[java队列]——PriorityBlockingQueue
PriorityBlockingQueue介绍
上一篇[[java队列]——PriorityQueue介绍了优先级队列,回顾一下PriorityQueue有哪些特点:
- 数据实现,自动扩容,无界
- 非线程安全
- 使用小顶堆作为实现,入队就是堆的插入,出队就是堆的删除堆顶元素
这一篇将介绍另一个优先级队列PriorityBlockingQueue,阻塞式的优先级队列,它相比PriorityQueue有以下特点
- 线程安全
- 阻塞
- 同样是数组实现,自动扩容,无界,同样底层是使用小顶堆实现优先级
PriorityBlockingQueue内部实现
基本属性
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//默认初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//数组最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//存储元素的数组
private transient Object[] queue;
//元素个数
private transient int size;
//比较器,用于比较元素之间的大小
private transient Comparator<? super E> comparator;
//可重入锁,控制并发安全
private final ReentrantLock lock;
//可重入锁上的非空条件
private final Condition notEmpty;
//???通过这个变量CAS更新成功的,就可以进行扩容
private transient volatile int allocationSpinLock;
//???据说用于序列化
private PriorityQueue<E> q;
结论:
- 数组实现
- 可重入锁+非空条件控制并发
- 为什么不需要非满条件,因为队列是无界的
- 结构基本与PriorityQueue一样,只是加多了锁,扩容使用了一个volatile变量CAS来控制并发。
构造方法
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
结论:
- 构造方法非常简单,直接看代码吧
入队
还是跟前面介绍的一样,阻塞队列一般有四个入队方法。基本都是调用了offer(E e)方法
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
//入队加锁
lock.lock();
int n, cap;
Object[] array;
//循环判断如果元素个数大于等于数组长度,则进行扩容。
while ((n = size) >= (cap = (array = queue).length))
//调用tryGrow完毕要继续判断,因为调用tryGrow返回并不代表当前线程已经扩容完毕,有可能是其他线程正在扩容,
//如果其他线程正在扩容,当前线程的tryGrow将会直接返回,要继续while循环判断,直到其他线程扩容完毕。
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
//入队方法与PriorityQueue类似,这里就不再介绍了
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
//入队成功,发出信号
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e); // never need to block
}
结论:
- 入队加锁,入队完毕,发出非空信号通知
- 循环判断数组元素个数是否已满,进行tryGrow扩容
扩容
private void tryGrow(Object[] array, int oldCap) {
//这里先释放锁,目的是由于已经达到扩容条件了,如果有多个线程正在尝试入队,这些线程将会被阻塞
//为了减少阻塞的线程,这里直接释放锁,使用CAS的方式进行扩容。
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
//CAS更新allocationSpinLock,更新成功才能进行扩容
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
//跟PriorityQueue一样的扩容增量规则
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
//扩容后解锁
allocationSpinLock = 0;
}
}
//若newArray为null,说明cas更新失败,其他线程正在扩容。这个时候将当前线程让出cpu
if (newArray == null) // back off if another thread is allocating
//yield是让当前线程让出cpu,从运行状态变为就绪状态,但并不代表,cpu就一定会先执行其他线程。因为是抢占式的,有可能又抢到cpu
Thread.yield();
//扩容成功,或者,线程让出cpu后返回回来,继续加锁
lock.lock();
//如果扩容成功并且就数组没有被替换过。。没看太懂这里??
if (newArray != null && queue == array) {
//队列更新为新数组
queue = newArray;
//复制新数组
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
结论:
- 扩容容量规则与PriorityQueue一致
- 扩容前先释放锁,为了减少阻塞线程的数量。
- 扩容前先释放锁,利用CAS来控制扩容。CAS更新失败,则让当前线程让出CPU
- 最后再次加锁,再确认是否扩容成功
出队
同样出队有四个方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//出队加锁,响应中断
lock.lockInterruptibly();
E result;
try {
//若出队队列为空,阻塞等待,dequeue与PriorityQueue的出队一样,这里就不介绍了
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
//解锁
lock.unlock();
}
return result;
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
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();
E result;
try {
while ( (result = dequeue()) == null && nanos > 0)
nanos = notEmpty.awaitNanos(nanos);
} finally {
lock.unlock();
}
return result;
}
结论:
- 出队加锁,若dequeue返回为空则一直阻塞,直到收到队列不为空的通知或者中断才会返回
- dequeue方法基本跟PriorityQueue一致
PriorityBlockingQueue总结
- 出入队线程安全,使用锁和非空条件实现
- 扩容线程安全,使用CAS
- 阻塞
- 与PriorityQueue同样是数组实现,自动扩容,无界,同样底层是使用小顶堆实现优先级,出入队的堆操作也一样