优先级阻塞队列PriorityBlockingQueue,不是FIFO队列,他要求使用者提供一个Comparetor比较器,或者队列内部元素实现Comparable接口,队头元素会是整个队列里的最小元素。
PriorityBlockingQueue的优先级特性的实现方式和PriorityQueue的实现一致。
PriorityBlockingQueue使用数组实现的最小堆结构,利用的原理是:在数组实现的完全二叉树中,父节点的下标为子节点下标除以2。
PriorityBlockingQueue是不定长的,会随着数据的增长会逐步扩容,其最大容量为Integer.MAX_VALUE-8,如果容量超出这个值,那么会产生OutOfMemoryError。
1, 成员变量
1 //数据容器
2 private transient Object[] queue;
3 //队列里的元素数量(因为队列尾端还有空节点,所以queue数组长度不代表数量)
4 private transient int size;
5 //比较器,如果没有才会使用元素的作为Cpomarable,那么在构造函数里初始化
6 private transient Comparator<? super E> comparator;
7 //所有公开操作的重入锁
8 private final ReentrantLock lock;
9 //非空条件监视器,在队列为空时,阻塞当前队列(队列具备孔融功能,所以没有notFull锁)
10 private final Condition notEmpty;
11 //用于重新分配queue大小的自旋锁
12 private transient volatile int allocationSpinLock;
13 //一个辅助队列,仅用于序列化
14 private PriorityQueue<E> q;
2, add/put/offer方法
add和put的实现最终都委托给了offer,所以这里会终点讲解offer方法的实现:
1 public boolean add(E e) {return offer(e);}
2 public void put(E e){offer(e);}
3 public boolean offer(E e) {
4 if(e == null) {
5 throw new NullPointerException();
6 }
7 //对入队操作上锁
8 final ReentrantLock lock = this.lock;
9 lock.lock();
10 int n, cap;
11 Object[] array;
12 //队列元素超长时,增加容量,该循环和tryGrow方法共同构成一个自旋锁
13 while((n = size) >= (cap = (array = queue).length)) {
14 tryGrow(array, cap);
15 }
16 try {
17 //根据是否设置了comparator来确定比较方式
18 Comparator<? super E> cmp = comparator;
19 //siftUp是最小堆的上浮方法,对于插入队尾n的元素e,确定它在最小堆的位置
20 if(cmp == null) {
21 siftUpComparable(n, e, array, cmp);
22 } else {
23 siftUpUsingComparator(n, e, array, cmp);
24 }
25 size = n + 1;
26 //在插入成功后,唤醒非空对象监视器阻塞线程,以执行出队方法
27 notEmpty.signal();
28 } finally {
29 lock.unlock;
30 }
31 return true;
32 }
通过offer的源码可以看出,它是线程安全的,在多线程环境中,lock的使用可以保证重新分配容量的操作和入队上浮操作在同一个时间点,只有一个线程在执行。
siftUpComparable/shiftUpUsingComparator这两个方法用于实现最小堆增加元素后的上浮操作,他们的实现和PriorityQueue完全一致。
3, tryGrow方法
tryGrow方法用于在size超出queue的长度时,尝试对数组进行扩容。
1 private void tryGrow(Object[] array, int oldCap) {
2 //释放锁,以便更多线程进入
3 lock.unlock();
4 Object[] newArray = null;
5 //allocationSpinLock为1时,会跳过该步骤,UNSAFE的CAS操作保证了这个变更的原子性
6 if(allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
7 try {
8 //体积增长策略,如果旧容量小于64,则容量翻倍再+2,反之扩容1/2
9 int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1));
10 //如果新容量超出最大容量,那么设置新容量为最大容量
11 if(newCap - MAX_ARRAY_SIZE > 0) {
12 //旧容量已经达到极限时,无法增长,则抛出OOM错误
13 int minCap = oldCap + 1;
14 if(minCap < 0 || minCap > MAX_ARRAY_SIZE) {
15 throw new OutOfMemoryError();
16 }
17 newCap = MAX_ARRAY_SIZE;
18 }
19 //构建新的队列数组
20 if(newCap > oldCap && queue == array) {
21 newArray = new Object[newCap];
22 }
23 } finally {
24 //重置自旋锁状态为0
25 allocationSpinLock = 0;
26 }
27 //new Array == null基本是因为allocationSpinLock为1,说明其他线程正在扩容,让出时间片
28 if(newArray == null) {
29 Thread.yield();
30 //重新上锁,保证queue的真正扩容阶段的线程安全
31 lock.lock();
32 if(newArray != null && queue == array) {
33 queue = newArray;
34 System.arraycopy(array, 0, new Array. 0, oldCap);
35 }
36 }
37 }
38 }
tryGrow方法的实现较为复杂,为什么要使用如此复杂的方式呢?原因是为了同时兼顾效率和安全性,在理解的时候需要注意:
1) 最耗时的操作是System.arraycopy操作,而计算出新容量和分配一个新的空数组,耗时不多,所以扩容操作只应该在必要的时候进行,在扩容的时候,需要释放锁从而使poll之类的出队操作可继续执行,假设有出队操作执行了,而导致newCap <= oldCap,则newArray会始终为null,不会扩容。
2) 由于PriorityBlockingQueue的出队与入队方法共用一把锁,unlock也可能会导致其他入队线程继续执行,为了保证扩容操作的线程安全,所以CAS+自旋锁来保证扩容计算只有单个线程执行。
3) Trhead.yield()方法,只会在自旋检测失败或者扩容失败的情况下调用,既然扩容失败,如果不让出时间片,那么该线程会继续执行tryGrow外部的while循环,在进行一次检测,这明显失效率的损失,所以调用yield,尽量让检测成功的线程去持有锁,当然,它不能保证百分之百让出成功。
4) queue != array是个特殊情况,发生System.arraycopy之前,是为了保证在扩容过程中,其他线程持有的旧queue不会造成线程不安全。
4, poll、take、peek方法
这三个方法用于出队,也都是使用重入锁来保证线程安全的。
1 public E take() throws InterruptedException {
2 final ReentrantLock lock = this.lock;
3 lock.lockInterruptibly();
4 E reault;
5 try {
6 //多次请求出队,请求失败则说明队列我empty,notEmpty阻塞当前线程
7 while((result = dequeue()) == null) {
8 notEmpty.await();
9 }
10 } finally {
11 lock.unlock();
12 }
13 return result;
14 }
15 public E poll() {
16 final ReentrantLock lock = this.lock;
17 lock.lock();
18 try {
19 //执行一次出队,并返回出队结果
20 return dequeue();
21 } finally {
22 lock.unlock();
23 }
24 }
25 public E poll(long timeout, TimeUnit unit) throws InterruptedException {
26 long nanos = unit.toNanos(timeout);
27 final ReentrantLock lock = this.lock;
28 lock.lockInterruptibly();
29 E result;
30 try {
31 //多次请求出队,timeout和timeUnit参数决定了出队失败后重试时长
32 while((result = dequeue()) == null && nanos > 0) {
33 nanos = notEmpty.awaitNanos(nanos);
34 }
35 } finally {
36 lock.unlock();
37 }
38 return result;
39 }
40 public E peek() {
41 final ReentrantLock lock = this.lock;
42 lock.lock();
43 try {
44 //查看队头,最小堆构成队列,队头始终未0
45 return (size == 0) ? null : (E)queue[0];
46 } finally {
47 lock.unlock();
48 }
49 }
5, dequeue方法
通过对出队的多个方法的解析,可以注意到,真正执行出队的方法是dequeue。dequeue在移除队头元素的同时,还需要维护最小堆的特性。
1 private E dequeue() {
2 int n = size - 1;
3 if(n < 0) {
4 return null;
5 } else {
6 Object[] array = queue;
7 //获取队头元素result
8 E result = (E)array[0];
9 //获取队尾元素
10 array[n] = null;
11 Comparator<? super E> cmp = comparator;
12 //siftDown操作会把队尾元素默认为队头位置,并且使用沉降操作来维护最小堆
13 if(cmp == null) {
14 siftDownComparable(0, x, array, n);
15 } else {
16 siftDownUsingComparator(0, x, array, n, cmp);
17 }
18 size = n;
19 return result;
20 }
21 }
siftDownComparable/siftDownUsingComparator这两个方法用于实现最小堆移除元素后的沉降操作,他们的实现和PriorityQueue完全一致。