概述
PriorityBlockingQueue:二叉堆结构优先级阻塞队列,FIFO,通过显式的lock锁保证线程安全,是一个线程安全的BlockingQueue,加入队列的数据实例按照指定优先级升序排列,这个规则通过赋值 实现了Comparator的字段或数据实例类实现Comparable接口自定义,都定义的情况下 字段比较器优先。它一个老牌的队列,在JDK1.5已经加入,如果队列加入的数据实例需要排序,这是个不错的选择。
核心属性和数据结构
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;//lock创建的 队列不为空控制条件。当队列为空阻塞,当新数据实例加入唤醒。
private transient volatile int allocationSpinLock;//初始值为0为可分配,用于控制扩容
private PriorityQueue q;//内部PriorityQueue引用,用于兼容序列化
二叉堆
PriorityBlockingQueue:内部数据由Object数组存储,这个数组本质上是一个二叉堆。
二叉堆是特殊的二叉树,它等于或接近完全二叉树,二叉堆满足特性:二叉堆的父节点总是子节点保持有序关系,并且每个节点的子节点也是二叉堆
当父节点的键值总是大于或等于任何一个子节点的键值,叫做最大堆,当父节点的键值总是小于或等于任何一个子节点的键值,叫做最小堆。
二叉堆总是用数组表示,如果根节点的位置是1,那么n节点的子节点分别是2n和2n+1,如果节点1的子节点在 2,3。 节点2的子节点在4,5 以此类推,根节点等于1,方便了子节点的推算,如果存储的数组下标从0开始。那么节点N的子节点分别是2n+1 和2(n+1),其父节点是(n-1)/2,PriorityBlockingQueue中使用的就是基于下标0的二叉堆。
源码分析
加入数据实体
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(array, cap);
try {//加入的节点需要根据优先级调整位置,使用字段比较器或节点比较器
Comparator<? super E> cmp = comparator;
if (cmp == null)//字段比较器 优先
siftUpComparable(n, e, array);//这里siftUp 理解为新加入的节点,经过比较尽量向数组后面移动
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;//统计节点数
notEmpty.signal();//唤醒阻塞
} finally {
lock.unlock();
}
return true;
}
siftUpComparable和siftUpUsingComparator实现逻辑相似,区别在于使用了不同的比较器。通常情况下使用节点比较器, 节点数据类实现Comparable接口。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
//到这 k是要写入的索引,key是要写入的值
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;
}
逻辑说明:
PriorityBlockingQueue是最小二叉堆,新插入的数据是子节点,找到父节点比较,大于等于父节点,跳出循环,小于父节点,交换位,key==0跳出循环,key小于0不可能出现。交换的过程是将父节点数据插入k位置,然后将父节点索引赋值给k。 等找到k的最终位置,新插入的数据插入 位置k。
假设插入Task 实例,按照 id=5,4,3,2,1的顺序添加,按照id大小排序。 刚添加后的数组数据id排列如 :1,2,4,5,3
就是说插入后的数据不是严格递增的,得到这个结果我也很惊讶,而经过测试 循环take取出的数据是严格递增的,能推测出take也做了排序处理,下面详细分析。
获取数据
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//可打断锁
E result;
try {
while ( (result = extract()) == null)//如果抽取的数据等于null,阻塞直到被唤醒。
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E extract() {
E result;
int n = size - 1;//获取数组有效数据最大索引值
if (n < 0)
result = null;
else {
Object[] array = queue;
result = (E) array[0];//获取0位置数据
E x = (E) array[n];//获取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;
}
//取出0位置的索引值后,从上到下调整父节点和子节点,达到排序的目的
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
//注意 k==0,x是array中最后面的有效数据,n是有效数据的最大索引值。
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {//k如果等于half 那么k的子节点将是n位置的数据,不管顺序问题,整体的数据节点在数组中是要前移一位的,n位置不存在
//将二叉堆最后位置n的数据放入0位置,从上到下调整父节点和子节点位置
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;
}
本质上是删除根节点,将二叉堆最后位置的节点放入根节点,然后从上到下调整父节点和子节点的位置。
为什么这样搞能保证有序?
下面来分析这个问题
假设id 1~9 按照逆序加入PriorityBlockingQueue,队列中顺序 是[1-id为1, 2-id为2, 4-id为4, 3-id为3, 7-id为7, 8-id为8, 5-id为5, 9-id为9, 6-id为6],组成的二叉堆如下图
由于PriorityBlockingQueue中的数组是最小二叉堆,叶子节点大于等于父节点,所以层级低(1层级高,9和6层级低)的节点与根节点差值越大
多次take二叉堆变化如下图
总结一下变化 每次取出根节点,放入尾部节点值,都将其所有父节点向上推动一层,如果没有父节点,将兄弟节点向上推动一层。由于 根节点与下一层的差值是最小的 并且上升后的根目录比子节点都小,所以每次取出的根节点都距离上次取出的根目录差值最小,所以取出的节点是严格有序的。
总结:本文重点理解二叉堆数据结构,和PriorityBlockingQueue借助二叉堆怎么实现排序的。
来源:CSDN
作者:chenchangqun11
链接:https://blog.csdn.net/chenchangqun11/article/details/104497507