PriorityBlockingQueue简介
PriorityBlockingQueue是一个具有优先级的阻塞队列。类似的阻塞队列用于实现生产消费模型比较方便,最常用的API有offer
,poll
,take
。
offer
:插入数据到队列中,不会阻塞,返回boolean表示成功还是失败poll
: 弹出队列中的第一个元素,非阻塞poll(timeout)
:如果队列为空会阻塞最长timeout时间take
:一直阻塞,直到有数据就弹出数据。
PriorityBlockingQueue组成
//默认容量11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
private transient Object[] queue;
//数据个数
private transient int size;
//比较器
private transient Comparator<? super E> comparator;
//锁
private final ReentrantLock lock = new ReentrantLock();
//信号量
private final Condition notEmpty = lock.newCondition();
/**
* Spinlock for allocation, acquired via CAS.
*/
private transient volatile int allocationSpinLock;
//用于序列化和反序列,一般时候为null
private PriorityQueue<E> q;
PriorityBlockingQueue
使用的是最小堆,也就是堆顶是最小值,一句comparator
来进行排序,默认使用自然排序。可以看到queue
是transient修饰的,无法序列化,序列化的时候会将queue
中的数据拷贝到q
中,为了适配老版本的PriorityBlockingQueue
。
offer
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
//加锁,线程安全
lock.lock();
int n, cap;
Object[] es;
//元素个数已经达到了队列容量,进行扩容
while ((n = size) >= (cap = (es = queue).length))
tryGrow(es, cap);
try {
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftUpComparable(n, e, es);//默认的排序方式
else
siftUpUsingComparator(n, e, es, cmp);//根据设置的cmp自定义排序方式
size = n + 1;
//通知不为空
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
扩容 tryGrow
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 先释放锁
Object[] newArray = null;
//cas成功就进行扩容
if (allocationSpinLock == 0 &&
ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
try {
//原始容量小于64时扩容为2*oldCap + 2,大于等于64时等于1.5*oldCap,最大值为Integer.MAX_VALUE
int growth = oldCap < 64 ? oldCap + 2 : oldCap >> 1;
int newCap = ArraysSupport.newLength(oldCap, 1, growth);
if (queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
if (newArray == null) //cas失败,说明有其它线程cas成功,则自己尽量让出cpu
Thread.yield();
lock.lock();//加锁
if (newArray != null && queue == array) {
//如果是cas成功的线程获将会走到这里,结束扩容
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
tryGrow
进来就先解锁是因为扩容是比较耗时的操作,所以使用cas提高性能,如果加锁就回到导致这个时候无法传入新数据。如果锁被cas失败的线程获取,就会自旋直到cas成功的线程获取到锁。因为tryGrow
是在offer方法的循环中,会再次判断容量是否足够。
插入数据到最小堆 siftUpComparable
自定义的Comparator插入也同理
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
//k初始是插入元素的个数,假设k = 3,元素数组形式[a,b,c]
//二叉堆形式:
// a
//b c
while (k > 0) {
//找到最后一个元素的父节点,相当于通过c的坐标找到a,即(2-1) >>> 1 = 0
int parent = (k - 1) >>> 1;
Object e = es[parent];
if (key.compareTo((T) e) >= 0)//比父节点大,也就是父节点的右孩子,直接插入到下标为k的位置
break;
//和父节点交换位置,继续和当前的父节点比较,直到比父节点大或者已经是root节点了
es[k] = e;
k = parent;
}
es[k] = key;
}
经典的最小堆算法。
poll(timeout)
直接看poll(timeout)
方法,因为这个方法其实包含了poll
和take
方法的原理
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)//poll方法没有循环,获取不到数据直接返回null
nanos = notEmpty.awaitNanos(nanos);//阻塞直到超时nanos或者其它线程调用了notEmpty.signal(),take中没有超时,直接使用notEmpty.await()
} finally {
lock.unlock();
}
return result;
}
小顶堆出队dequeue
private E dequeue() {
// assert lock.isHeldByCurrentThread();
final Object[] es;
final E result;
//头结点出队,然后调整堆,保持小顶堆
if ((result = (E) ((es = queue)[0])) != null) {
final int n;
final E x = (E) es[(n = --size)];//堆尾元素
es[n] = null;
if (n > 0) {
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // 自上而下,直到没有子节点
while (k < half) {
int child = (k << 1) + 1; // 从左孩子开始
Object c = es[child];
int right = child + 1;//右孩子
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];//让c为比较小的那个节点
if (key.compareTo((T) c) <= 0) //最后一个节点和比较小的那个节点比较,小于等于小的那个节点就退出循环,并将最后一个节点赋值给当前比较的子节点的父节点
break;
//最后一个节点比较大,将两个孩子节点中比较小的节点赋值到父节点
es[k] = c;
//继续往比较小的子树下一层找,然后继续和最后一个节点进行比较,可以想象成当前节点来到了比较小的节点的位置,继续往后比较
k = child;
}
es[k] = key;
}
小顶堆的移除看注释不太好弄明白,下面画个图举个例子
自上而下的和尾节点进行比较,每次选择较小的那个节点所在的子树继续比较,每次将最小的那个节点赋值给父节点。虽然最后一个节点还保留这,但是由于size
已经减1了,所以引用不到。
一个特殊的例子
在这个例子首先会自定义一个Comparable,并且这个Comparable会认为所有的对象都是相同的。
特殊的Comparable类:
private static class TestComparable implements Comparable<TestComparable> {
int value = 0;
public TestComparable(int value) {
this.value = value;
}
@Override
public int compareTo(TestComparable o) {
return 0;
}
}
主程序:
public static void main(String[] args) {
PriorityBlockingQueue<TestComparable> queue = new PriorityBlockingQueue<>(10);
for (int i = 0; i < 10; i++) {
queue.add(new TestComparable(i));
}
System.out.print("[");
for (int i = 0; i < 10; i++) {
System.out.print(queue.poll().value);
}
System.out.println("]");
}
以上程序最终的打印结果是 [0987654321] ,可以看到第一个打印的是第一个插入的节点,之后打印的顺序是和插入顺序反过来的,在认真回想一下小顶堆出队dequeue,就能更好的理解PriorityBlocking的出队逻辑。