PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素。其内部是使用平衡二叉树堆实现的,所以直接遍历队列元素不保证有序。默认使用对象的compareTo方法提供比较规则,如果你需要自定义比较规则可以自定义comparators
属性:
//默认初始化容量大小
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//Object数组,作为队列,假设队列不空的话,代表平衡二叉堆,对于每一个节点n,他的两个孩子为2n+1和2n+2
//优先级队列的排序是按照排序器的,或者按照元素的自然排序
//如果排序器为空,对于每一个节点n,都有他的每一个后代d,n<=d,最小的节点是queue[0]
transient Object[] queue;
//优先级队列的元素个数
private int size = 0;
//比较器,如果优先级队列使用元素的自然排序则为null
private final Comparator<? super E> comparator;
//公共操作使用的锁
private final ReentrantLock lock;
//当队列为空时条件阻塞队列
private final Condition notEmpty;
//cas变量,用来控制同一时间只有一个线程可以扩容
private transient volatile int allocationSpinLock;
构造函数:
第一种:传入一个初始容量和比较器,如果不传,初始容量默认为11,比较器默认为null
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];
}
第二种:传入一个集合,将集合中的元素插入到队列中
public PriorityBlockingQueue(Collection<? extends E> c) {
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
boolean heapify = true; // true if not known to be in heap order
boolean screen = true; // true if must screen for nulls
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
heapify = false;
}
else if (c instanceof PriorityBlockingQueue<?>) {
PriorityBlockingQueue<? extends E> pq =
(PriorityBlockingQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
screen = false;
if (pq.getClass() == PriorityBlockingQueue.class) // exact match
heapify = false;
}
Object[] a = c.toArray();
int n = a.length;
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, n, Object[].class);
if (screen && (n == 1 || this.comparator != null)) {
for (int i = 0; i < n; ++i)
if (a[i] == null)
throw new NullPointerException();
}
this.queue = a;
this.size = n;
if (heapify)
heapify();
}
主要方法:
offer操作:
如果要插入的元素为空,抛出异常,加锁,保证同一时间只有一个线程可以插入
如果元素个数大于容量值,进行扩容
如果不需要扩容,则拿到比较器,如果为null,则使用元素的自然排序,如果不为null,使用比较器规则来插入元素
然后size+1,因为插入之后至少有一个元素,所以唤醒元素时因为队列为空而进入条件队列的线程,最后释放锁
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(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
扩容逻辑:
扩容之前先释放锁,这样是为了扩容时能进行出队、入队、读取操作,提高并发性,
接着对allocationSpinLock这个volatile变量进行cas操作,保证同一时间只有一个线程可以扩容
判断旧容量是否小于64,如果小于64则扩容2倍的旧容量+2,如果不小于64则扩容旧容量的一半,并且最大为MAX_ARRAY_SIZE
扩容完成之后,将allocationSpinLock置为0,注意,这时候还没有将数组替换,所以释放allocationSpinLock锁之后,即使另一个线程去扩容还是扩容一样的大小,就是说虽然会走两遍扩容流程,但是两次扩容下来,大小是一样的,而且替换数组时会判断queue是否与array相等,已经扩容过的情况下不会相等,所以还是只扩容一遍
如果线程没有抢到allocationSpinLock锁,则会让出cpu时间片,但是无法保证,可能扩容线程往下获取锁,可能当前线程获取锁,当前线程获取锁之后会跳出方法,但是扩容的方法是在一个while循环中,进来还是会释放锁,这又给了扩容线程机会
扩容线程获取到锁之后会替换旧数组,并拷贝旧数组
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // must release and then re-acquire main lock//(1)
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {//(2)
try {
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;
}
}
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
建堆逻辑:
当队列元素个数>0便判断插入位置,第一次拿(队列元素个数-1)/2的位置parent,如果parent位置的元素大于要插入的元素,则将parent位置的元素放入k位置(k为元素个数,作为下标就是前k个元素的下一个位置),
然后将parent的值赋给k,直到找到比要插入的元素小的位置,这就形成了一个最小堆。下面的entity.getYLonCur()方法在比较的时候用了提供的比较器,其他逻辑一样
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;
}
poll操作:
加锁之后调用dequeue方法
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
dequeue方法:
如果队列为空,直接返回null,拿到队列中的根节点元素,作为结果返回
拿到堆中尾节点的元素,重新构建最小堆或者最大堆
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
重新构建最小堆的逻辑:
从根节点开始,比较层数为元素个数/2--half,第一次拿出根节点的左右子树中较小的元素,和尾节点元素比较,较小的放到根节点位置,然后从根节点较小的左(left)右(right)子树开始找left或者right的子树中找较小的元素和尾节点元素比较,直到比较层数为half
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
举例:
第一次poll时,拿到根节点元素k=0,key=2作为结果返回,拿到尾节点元素x=11,将尾节点置null,如图(1)
从根节点开始,找到左右子树中较小的元素child=1,c=4,因为4<11,所以根节点变成4,k=child=1,如图(2)
这时k<half=2,继续循环,找到child的左右子树较小的元素child=4,c=8,8<11,所以child位置变成8,k=4,如图(3)
4,这时k>half,结束循环,将k位置变成11,如图(4),并返回
take操作:
先加锁,加锁过程中被其他线程中断会抛出异常
调用dequeue返回根节点元素,如果队列为空,调用notEmpty.await()阻塞自己,知道其他线程调用offer方法,offer方法成功后会调用notEmpty.signal()激活一个阻塞在notEmpty条件队列里的线程
如果队列为空,则会阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
计算队列元素个数:
先加锁,保证调用方法时没有入队和出队操作,并且保证size变量的内存可见性
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return size;
} finally {
lock.unlock();
}
}