基于JDK1.8详细介绍了PriorityBlockingQueue的底层源码实现,包括小顶堆和优先级排序的原理,以及入队列、出队列等操作源码。
文章目录
1 PriorityBlockingQueue的概述
public class PriorityBlockingQueue< E >
extends AbstractQueue< E >
implements BlockingQueue< E >, Serializable
PriorityBlockingQueue来自于JDK1.5的JUC包,是一个支持并发操作的无界阻塞队列,其最大的特点是每一次出队列的元素都优先级最高的元素!
底层物理结构直接采用了一个数组,逻辑结构则是实现了一个小顶堆的排序树,默认情况下元素采取自然顺序排序,当然也按照可以自己指定比较器来对元素进行排序,基于堆的特点,不能保证同优先级元素的顺序,实际上内部仅仅维护了一个偏序关系。
内部只有一个锁lock和一个条件队列notEmpty,生产和消费线程都需要获取lock锁,而notEmpty用于消费线程的等待和唤醒,因为是无界队列,生产线程不需要等待和唤醒!
实现了Serializable接口,支持序列化,没有实现Cloneable,不支持克隆!
不支持null元素!
PriorityBlockingQueue相比于ArrayBlockingQueue和LinkedBlockingQueue的复杂度更高,主要是对于小顶堆数据结构的理解,只有明白了小顶堆的原理,才能看懂PriorityBlockingQueue的源码!本文没有讲解堆结构和堆排序的原理,因为在前的文章中都已经分析了,如果有不了解的“堆”的,应该先看看这两篇文章:10种常见排序算法原理详解以及Java代码的完全实现,其中有关于堆排序的详解。数据结构—堆(Heap)的原理介绍以及Java代码的完全实现,其中有关于大顶堆和小顶堆的介绍和构建实现!如果不明白的堆结构的特性而直接看下面的源码分析那么应该会有很多看不明白的地方!
2 PriorityBlockingQueue的原理
2.1 主要属性
概述中我们说PriorityBlockingQueue是无界阻塞队列,但是由于底层采用数组,很明显最大元素数量为Integer.MAX_VALUE,即实际上还是“有界”的,当容量资源被耗尽时试图执行 add 操作也将失败(导致 OutOfMemoryError)!它的小顶堆是逻辑实现因此不存在实际结构!
内部的MAX_ARRAY_SIZE实际上并不是最大长度,实际最大数组长度可以超过该值,在ArrayList文章中具有同名变量的介绍,在PriorityBlockingQueue中仅仅扩容的时候会用到MAX_ARRAY_SIZE。
只有一个锁lock,生产和消费都需要获取同一个锁,一个条件变量notEmpty,用于消费线程的等待和唤醒,生产线程不会等待,因为队列是“无界”的,可以一直入队。另外还有一个标志位allocationSpinLock,用来手动实现CAS锁,在扩容的时候会用到!某些注释比较晦涩,但是没关系,后面源码中会仔细讲解!
/**
* 默认数组容量11
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 要分配的数组的最大大小。尝试分配较大的数组可能会导致内存错误OutOfMemoryError:请求的数组大小超过 VM 限制
* 实际最大数组长度可以超过该值,没有特别实际的意义
* 具体详见:https://blog.csdn.net/weixin_43767015/article/details/106490024,内部具有同名变量的介绍
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 底层物理数据结构,一个数组,通过该数组可以实现小顶堆的逻辑数据结构
*/
private transient Object[] queue;
/**
* 元素数量
*/
private transient int size;
/**
* 自定义比较器,如果为null,则使用元素的自然顺序比较
*/
private transient Comparator<? super E> comparator;
/**
* 锁实例,生产和消费线程都需要获取该lock锁
*/
private final ReentrantLock lock;
/**
* 条件变量实例,消费线程的等待和唤醒
*/
private final Condition notEmpty;
/**
* 用于手动实现自旋锁的标志位,在tryGrow扩容方法中会用到
*/
private transient volatile int allocationSpinLock;
/**
* 仅用于序列化和反序列化操作,为了兼容老版本
*/
private PriorityQueue<E> q;
2.2 构造器
2.2.1 PriorityBlockingQueue()
public PriorityBlockingQueue()
用默认的初始容量11创建一个 PriorityBlockingQueue,并根据元素的自然顺序对其元素进行排序。
/**
* 用默认的初始容量11创建一个 PriorityBlockingQueue,并根据元素的自然顺序对其元素进行排序。
*/
public PriorityBlockingQueue() {
//内部调用另一个构造器
this(DEFAULT_INITIAL_CAPACITY, null);
}
2.2.2 PriorityBlockingQueue(initialCapacity)
PriorityBlockingQueue(int initialCapacity)
使用指定的初始容量创建一个 PriorityBlockingQueue,并根据元素的自然顺序对其元素进行排序。
/**
* 使用指定的初始容量创建一个 PriorityBlockingQueue,并根据元素的自然顺序对其元素进行排序。
*
* @param initialCapacity 指定初始容量
* @throws IllegalArgumentException 如果 指定初始容量 小于 1
*/
public PriorityBlockingQueue(int initialCapacity) {
//内部调用另一个构造器
this(initialCapacity, null);
}
2.2.3 PriorityBlockingQueue(initialCapacity, comparator)
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator)
使用指定的初始容量创建一个 PriorityBlockingQueue,并根据指定的比较器对其元素进行排序。
可以看到这里的initialCapacity并没有判断是否超过了MAX_ARRAY_SIZE,因此即使超过了MAX_ARRAY_SIZE也会进行初始化的,这就是最大容量有可能比MAX_ARRAY_SIZE更大的原因!
/**
* 使用指定的初始容量创建一个 PriorityBlockingQueue,并根据指定的比较器对其元素进行排序。
*
* @param initialCapacity 指定初始容量
* @param comparator 指定比较器
* @throws IllegalArgumentException 如果 指定初始容量 小于 1
*/
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
//initialCapacity的校验
if (initialCapacity < 1)
throw new IllegalArgumentException();
//初始化lock锁
this.lock = new ReentrantLock();
//初始化条件变量
this.notEmpty = lock.newCondition();
//初始化比较器
this.comparator = comparator;
//使用initialCapacity初始化底层数组
this.queue = new Object[initialCapacity];
}
2.2.4 PriorityBlockingQueue( c )
public PriorityBlockingQueue(Collection<? extends E> c)
创建一个包含指定集合元素的PriorityBlockingQueue。如果指定集合是一个 SortedSet 或 PriorityQueue,则此优先级队列将按照相同顺序进行排序。否则,此优先级队列将根据此元素的自然顺序进行排序。
大概步骤为:
- 首先初始化lock锁和notEmpty条件变量;
- 设置两个标志位:heapify是否构建堆的标志,true 是 false 否,默认true;screen是否必须检测null的标志,true 是 false 否,默认为false;
- 如果指定集合c属于SortedSet类型或者子类,即排序集合,获取比较器,不需要构建小顶堆,heapify=false;
- 否则,如果指定集合属于PriorityBlockingQueue类型或者子类,获取比较器,如果如果类型就是PriorityBlockingQueue类型,不需要构建小顶堆,heapify=false;
- 获取指定集合的数组a,获取数组长度n,a转换为Object[].class类型,
- 如果(指定集合不属于属于SortedSet类型或者子类或者不属于PriorityBlockingQueue类型或者子类),并且(n为1 或者 指定比较器不为null),那么需要检测null,因为此时数组中可能存在null元素;
- 初始化底层数组queue = a,初始化数量size = n;
- 如果heapify为true,表示需要构建小顶堆,那么调用heapify方法根据整个数组构建小顶堆。
/**
* 创建一个包含指定集合元素的PriorityBlockingQueue。
* 如果指定集合是一个 SortedSet 或 PriorityQueue,则此优先级队列将按照相同顺序进行排序。
* 否则,此优先级队列将根据此元素的自然顺序进行排序。
*
* @param c 指定集合
* @throws ClassCastException 如果根据优先级队列的顺序,指定集合的元素无法与本集合的元素进行比较
* @throws NullPointerException 如果指定 集合 或其任意元素为 null
*/
public PriorityBlockingQueue(Collection<? extends E> c) {
//初始化lock锁
this.lock = new ReentrantLock();
//初始化条件变量
this.notEmpty = lock.newCondition();
//是否构建堆的标志 true 是 false 否
boolean heapify = true; // true if not known to be in heap order
//是否必须检测null的标志 true 是 false 否
boolean screen = true; // true if must screen for nulls
/*如果指定集合属于SortedSet类型或者子类,即排序集合,不需要构建小顶堆*/
if (c instanceof SortedSet<?>) {
//转换为SortedSet
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
//获取ss的比较器
this.comparator = (Comparator<? super E>) ss.comparator();
//heapify置为false,不需要构建小顶堆
heapify = false;
}
/*如果指定集合属于PriorityBlockingQueue类型或者子类*/
else if (c instanceof PriorityBlockingQueue<?>) {
//转换为PriorityBlockingQueue
PriorityBlockingQueue<? extends E> pq =
(PriorityBlockingQueue<? extends E>) c;
//获取比较器
this.comparator = (Comparator<? super E>) pq.comparator();
//screen置为false
screen = false;
//如果类型就是PriorityBlockingQueue,不需要构建小顶堆
if (pq.getClass() == PriorityBlockingQueue.class) // exact match
//heapify置为false
heapify = false;
}
//获取元素数组a
Object[] a = c.toArray();
//获取长度n
int n = a.length;
// If c.toArray incorrectly doesn't return Object[], copy it.
//如果a不是Object[].class类型,那么复制元素,使其成为Object[].class类型
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, n, Object[].class);
//如果指定集合不属于属于SortedSet类型或者子类 或者 不属于PriorityBlockingQueue类型或者子类
//并且 n为1 或者 比较器不为null
//那么需要检测null
if (screen && (n == 1 || this.comparator != null)) {
//循环遍历指定集合的元素数组,检测null
for (int i = 0; i < n; ++i)
if (a[i] == null)
throw new NullPointerException();
}
//初始化底层数组
this.queue = a;
//初始化数量
this.size = n;
//如果heapify为true,表示需要构建小顶堆
if (heapify)
//根据整个数组构建小顶堆
heapify();
}
2.2.4.1 heapify根据数组构建小顶堆
heapify方法虽然只有在 PriorityBlockingQueue(Collection<? extends E> c)构造器中被调用到,但是却是一个非常重要的方法,因为这实际上就是堆排序的第一步:根据整个数组已存在的元素构建小顶堆。
这一步的原理我们在堆排序的那一部分已经讲的很清楚了,它们的原理就是一样的,代码也差不多,在此不做赘述!10种常见排序算法原理详解以及Java代码的完全实现—堆排序!
/**
* 只有在PriorityBlockingQueue(c)的构造器中被调用到
* 根据整个数组构建小顶堆,这实际上就是堆排序过程的第一部分
*/
private void heapify() {
//获取内部数组
Object[] array = queue;
//获取size数组长度
int n = size;
//最后一个非叶子结点的索引位置
int half = (n >>> 1) - 1;
//获取指定比较器
Comparator<? super E> cmp = comparator;
//如果指定比较器为null,那么使用自然排序比较
if (cmp == null) {
//开始构建,代码和套路都是一样的,在我们自己的实现小顶堆中也是采用了这种方法
/*
* i从最后一个非叶子结点的索引开始,递减构建,直到i=-1结束循环
* 这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1,这是利用了完全二叉树的性质
*/
for (int i = half; i >= 0; i--)
//调用siftDownComparable构建从某个非叶子结点开始的某个二叉树分支的小顶堆
//传入当前非叶子结点的索引,索引结点,数组,数组长度
siftDownComparable(i, (E) array[i], array, n);
}
/*否则,使用比较器排序*/
else {
//开始构建,代码和套路都是一样的,在我们自己的实现小顶堆中也是采用了这种方法
/*
* i从最后一个非叶子结点的索引开始,递减构建,直到i=-1结束循环
* 这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1,这是利用了完全二叉树的性质
*/
for (int i = half; i >= 0; i--)
//调用siftDownUsingComparator构建从某个非叶子结点开始的某个二叉树分支的小顶堆
//传入当前非叶子结点的索引,索引结点,数组,数组长度,比较器
siftDownUsingComparator(i, (E) array[i], array, n, cmp);
}
}
2.2.4.1.1 siftDownComparable/siftDownUsingComparator对某结点向下构建部分小顶堆
在heapify方法中,实际上就是从最小非叶子结点开始,循环的对每一个非叶子结点向下构建部分小顶堆,循环完毕,堆结构构建完毕!
siftDownComparable/siftDownUsingComparator就是封装的对某结点构建部分小顶堆的代码逻辑,siftDownComparable使用自然顺序比较,而siftDownUsingComparator使用指定比较器比较!
在后面的消费数据的方法中,比如take、poll、remove等方法,它们就是直接根据某结点构建部分小顶堆,而不必整个构建堆,因此也是调用这两个方法!
这一部分源码和我们在堆排序的构建堆部分自己实现的源码也都差不多,原理同样是一样的,在此不做赘述!10种常见排序算法原理详解以及Java代码的完全实现—堆排序!
/**
* 从当前非叶子结点开始构建与它关联结点的小顶堆关系
* 使用自然顺序比较
*
* @param k 当前非叶子结点的索引
* @param x 索引结点
* @param array 数组
* @param n 堆的大小,堆结点数量
*/
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
//堆的大小大于0
if (n > 0) {
//key记录索引结点
Comparable<? super T> key = (Comparable<? super T>) x;
//获取最后一个非叶子结点的下一个结点的索引half
int half = n >>> 1; // loop while a non-leaf
/*
* while循环,如果k小于half,表示后面的索引位置还有非叶子结点,可能需要调整位置,那么继续循环
* 否则,表示后面的索引位置没有有非叶子结点,不需要调整位置,退出循环
*/
while (k < half) {
//获取结点的子结点的索引,这里是左子结点
int child = (k << 1) + 1; // assume left child is least
//获取左子结点
Object c = array[child];
//获取右子结点索引,实际上可能没有右子结点
int right = child + 1;
//如果right小于n,说明该结点具有右子结点
//并且 如果左子结点大于右子结点
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
//child指向右子结点索引,c指向右子结点
c = array[child = right];
//比较key结点和k结点的最小的子结点c的大小,如果key小于等于c,满足小顶堆的条件,直接终止循环
if (key.compareTo((T) c) <= 0)
break;
//否则如果key大于c,即父结点大于最小的直接子结点,不满足小顶堆的条件
//那么将k位置的值置为c,即原c提升高度
array[k] = c;
//k指向最小的子结点c的索引位置,即k增大(降低高度)
k = child;
//下一次循环时将会以这一次循环中最小的子结点作为父结点,继续向下递归调整位置
// 直到key结点和k结点的最小的子结点c,或者k大于等于half为止,说明调整完毕
}
//循环结束之后,最后k的位置置为key,这个位置的key大于等于它的直接父结点(如果存在),小于等于它的两个直接子结点(如果存在)
array[k] = key;
}
}
/**
* 从当前非叶子结点开始构建与它关联结点的小顶堆关系
* 使用指定比较器顺序比较
*
* @param k 当前非叶子结点的索引
* @param x 索引结点
* @param array 数组
* @param n 堆的大小,堆结点数量
* @param cmp 指定比较器
*/
private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
int n,
Comparator<? super T> cmp) {
//堆的大小大于0
if (n > 0) {
//获取最后一个非叶子结点的下一个结点的索引half
int half = n >>> 1;
/*
* while循环,如果k小于half,表示后面的索引位置还有非叶子结点,可能需要调整位置,那么继续循环
* 否则,表示后面的索引位置没有有非叶子结点,不需要调整位置,退出循环
*/
while (k < half) {
//获取结点的子结点的索引,这里是左子结点
int child = (k << 1) + 1;
//获取左子结点
Object c = array[child];
//获取右子结点索引,实际上可能没有右子结点
int right = child + 1;
//如果right小于n,说明该结点具有右子结点
//并且 如果左子结点大于右子结点
if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
//child指向右子结点索引,c指向右子结点
c = array[child = right];
//比较x结点和k结点的最小的子结点c的大小,如果x小于等于c,满足小顶堆的条件,直接终止循环
if (cmp.compare(x, (T) c) <= 0)
break;
//否则如果key大于c,即父结点大于最小的直接子结点,不满足小顶堆的条件
//那么将k位置的值置为c,即原c提升高度
array[k] = c;
//k指向最小的子结点c的索引位置,即k增大(降低高度)
k = child;
//下一次循环时将会以这一次循环中最小的子结点作为父结点,继续向下递归调整位置
// 直到key结点和k结点的最小的子结点c,或者k大于等于half为止,说明调整完毕
}
//循环结束之后,最后k的位置置为key,这个位置的key大于等于它的直接父结点(如果存在),小于等于它的两个直接子结点(如果存在)
array[k] = x;
}
}
2.3 入队操作
由于是无界队列,因此入队操作不会因为队列满了而被阻塞,但是如果在容量/内存资源被耗尽时试图执行入队操作也将失败(导致OutOfMemoryError)。
2.3.1 offer(e)方法
public boolean offer(E e)
将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,一定会返回true。
这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用offer方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!
如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较,那么抛出ClassCastException;如果指定元素为 null,那么抛出NullPointerException。
大概步骤为:
- e的null校验,如果e为null则抛出NullPointerException;
- 不可中断的等待获取lock锁,即不响应中断。获取到锁之后,才进行下面的步骤;开启一个
- while循环,初始化n=size,array = queue,cap=array.length,如果如果n 大于等于 数组长度cap ,表示数组容量已满,需要扩容:
- 调用tryGrow方法尝试扩容,tryGrow方法结束进入下一次循环重新获取数据进行比较。
- 如果不需要扩容或者扩容完毕,那么结束循环。获取指定比较器cmp,如果cmp为null,调用siftUpComparable根据新结点向上构建小顶堆,使用自然排序;如果cmp不为null,调用siftUpComparable根据新结点向上构建小顶堆,使用指定比较器排序;
- 构建完毕,size自增1;然后尝试唤醒一个在notEmpty中等待的消费线程;
- 最终在finally中一定会释放锁,如果前面的代码没有抛出异常,那么返回true。
/**
1. 将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,一定会返回true。
2. 3. @param e 指定元素
4. @return true
5. @throws ClassCastException 如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较
6. @throws NullPointerException 如果指定元素为 null
*/
public boolean offer(E e) {
//e的null检测
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
//不可中断的等待获取lock锁,即不响应中断
lock.lock();
int n, cap;
Object[] array;
/*
* n=size,array = queue,cap=array.length
* 循环,如果n 大于等于 数组长度cap ,表示数组容量已满,需要扩容;
* 否则结束循环,表示扩容完毕或者不需要扩容
*/
while ((n = size) >= (cap = (array = queue).length))
//调用tryGrow方法扩容,只有在tryGrow方法中被调用,即只有生产者线程会调用tryGrow方法
tryGrow(array, cap);
//到这一步,一定是不需要扩容或者扩容完毕了,一定是获取到了锁
try {
//获取指定比较器cmp
Comparator<? super E> cmp = comparator;
//如果cmp为null
if (cmp == null)
//调用siftUpComparable根据新结点构建小顶堆,使用自然排序
siftUpComparable(n, e, array);
//如果cmp不为null
else
//调用siftUpComparable根据新结点构建小顶堆,使用指定比较器排序
siftUpUsingComparator(n, e, array, cmp);
//size自增1
size = n + 1;
//尝试唤醒一个在notEmpty中等待的消费线程
notEmpty.signal();
} finally {
//解锁
lock.unlock();
}
//返回true
return true;
}
2.3.1.1 扩容机制
在插入数据时,如果发现堆大小大于等于数组长度,说明容量满了,此时需要扩容,PriorityBlockingQueue的扩容机制并不是某一个方法,而两个方法配合完成的!
大概步骤为:
- 首先在offer方法中获取lock锁;
- 开启一个while循环,如果此时的堆大小size大于等于底层数组容量,那么继续循环:
- 循环中调用tryGrow方法尝试扩容:
- 首先就释放获取到的锁,设置newArray保存新数组,默认为null;
- 如果allocationSpinLock为0,并且尝试CAS的将allocationSpinLock的值从0变成1成功,那么进入if代码块中,if代码块用于计算新容量和尝试初始化新数组,多个线程竞争的时候,同一时刻只有一条线程能够成功进入if代码块:
- 计算新容量newCap,原容量越小增长的越快,这样可以降低扩容次数。如果oldCap小于64,那么 newCap = oldCap + oldCap + 2 ,即扩容增量为 oldCap + 2;如果oldCap大于等于64,那么 newCap = oldCap + oldCap >> 1 ,即扩容增量为oldCap >> 1(老容量的一半);
- 当oldCap 位于 [1431655760,2147483647]区间时,计算出来的 newCap - MAX_ARRAY_SIZE 将会大于0,这表示表示容量可能溢出了;
- 计算最小容量minCap = 旧容量+1,如果minCap 小于0,这表示此前的oldCap就是Integer.MAX_VALUE或者 minCap 大于 MAX_ARRAY_SIZE,即此前的oldCap范围是[Integer.MAX_VALUE-8,Integer.MAX_VALUE-1]。这两种情况就是容量溢出的情况,满足一种即抛出OutOfMemoryError异常;
- 到这一步,说明此前oldCap范围是[1431655760,Integer.MAX_VALUE-9],这时newCap直接赋值为MAX_ARRAY_SIZE,即最大容量。
- 如果新容量newCap大于旧容量oldCap,并且如果底层数组queue还是目前的数组array,说明还没有线程成功扩容过,那么新建数组长度为newCap,赋给newArray;
- 无论此前是否发生异常,在finally中将标志位allocationSpinLock重置为0,同一时刻只有一个线程执行到这里,因此不需要CAS操作就可保证是线程安全的。这里将allocationSpinLock重置为0之后,后续的线程在CAS时就可能成功进入if代码块。
- 到这一步,可能是:线程将if代码块执行完毕,此时该扩容线程中的newArray不为null,或者线程没有争取到扩容资格,没有执行if代码块,此时对应线程中的newArray为null;
- 如果newArray为null,说明是没有执行if代码块的线程,那么此线程尽量让出CPU的执行权,回到RUNNABLE状态,让自己和其他多个线程重新争夺cpu执行权。其目的是让执行了if代码块的线程先获取CPU的执行权向下执行,因为下一步就是获取锁的操作,但是yield方法并不一定会释放成功,毕竟java线程最终是调用操作系统的资源生成的,充满了不确定性。
- 重新获取lock锁,根据上面的步骤,大概率是执行了if代码块的线程的线程先获取到,但这不是绝对的。下面的步骤都是获取锁之后的操作,是线程安全的;
- 如果newArray不为null,说明成功执行了if代码块,并且如果底层数组queue还是目前的数组array,说明还没有线程成功扩容过,那么在if代码块中为queue赋值,并转移数据,tryGrow方法结束。一次扩容过程中进入过if代码块并且第一个获取锁的线程,才会走到这一步。
- 上面的if不满足,同样tryGrow方法结束。没有执行if代码块的线程,或者进入过if代码块但是和别的线程扩容冲突的线程,都将会走到这一步。
- tryGrow方法执行完毕,继续下一次循环,实际上是做一次判断:如果此时堆的大小n 大于等于底层数组长度cap ,表示数组容量已满,需要扩容,那么继续执行tryGrow方法;否则结束循环。
- 循环中调用tryGrow方法尝试扩容:
public boolean offer(E e) {
//…………
//扩容机制
//不可中断的等待获取lock锁,即不响应中断
lock.lock();
int n, cap;
Object[] array;
/*
* n=size,array = queue,cap=array.length
* 循环,如果n 大于等于 数组长度cap ,表示数组容量已满,需要扩容;否则结束循环
*/
while ((n = size) >= (cap = (array = queue).length))
//调用tryGrow方法扩容,只有在tryGrow方法中被调用,即只有生产者线程会调用tryGrow方法
tryGrow(array, cap);
//…………
}
/**
1. 尝试增加足够多的数组容量,扩容时没有加锁扩,实际上是采用的一个CAS锁控制只有一个线程能够成功
2. 并且CAS方法并没有采用循环重试机制,但是没关系,因为如果CAS失败导致扩容失败,那么在外面的offer方法的下一次while循环中,
3. 会判断此时size可能还会大于等于底层数组的容量,如果是然后又会进入该方法,这就相当于一个循环了,如果不是说明其他线程已经扩容成功
4. 5. @param array 原底层数组
6. @param oldCap 原底层数组容量
*/
private void tryGrow(Object[] array, int oldCap) {
//首先释放获取到的锁
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
/*
* 下面是第一步扩容的逻辑,用于计算新数组,这里i没有加锁实际上是采用的一个CAS锁
* 如果allocationSpinLock为0 并且 尝试CAS的将allocationSpinLock的值从0变成1成功,那么可以进入if代码块中
* 在扩容活动的过程中,如果有多条线程扩容,这里的CAS操作并不能保证最终只有一条线程能够进入if代码块
* 但是能保证同时只有一个线程能够进入if代码块中,失败的线程进入下一步,或者某条线程的if代码块执行完毕之后,后续线程再进入if代码块
* 这里的if并不能保证,最终扩容的安全,是在if下面 重新获取锁之后的赋值时才保证的
*/
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
/*
* 计算新容量newCap,原容量越小增长的越快,这样可以降低扩容次数,因此有两种情况:
* 1 如果oldCap小于64,那么 newCap = oldCap + oldCap + 2 ,即扩容增量为 oldCap + 2
* 2 如果oldCap大于等于64,那么 newCap = oldCap + oldCap >> 1 ,即扩容增量为oldCap >> 1(老容量的一半)
*/
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
//当oldCap 位于 [1431655760,2147483647]区间时,计算出来的 newCap - MAX_ARRAY_SIZE 将会大于0
//如果newCap减去MAX_ARRAY_SIZE大于0,表示容量可能溢出了
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
//计算最小容量minCap = 旧容量+1
int minCap = oldCap + 1;
//如果minCap 小于0,这表示此前的oldCap就是Integer.MAX_VALUE
//或者 minCap 大于 MAX_ARRAY_SIZE,这表示此前的oldCap范围是[Integer.MAX_VALUE-8,Integer.MAX_VALUE-1]
//这两种情况就是容量溢出的情况,满足一种即抛出异常
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
//那么抛出OutOfMemoryError异常
throw new OutOfMemoryError();
//到这一步,说明此前oldCap范围是[1431655760,Integer.MAX_VALUE-9]
//这时newCap直接赋值为MAX_ARRAY_SIZE,即最大容量
newCap = MAX_ARRAY_SIZE;
}
/*
* 如果新容量newCap大于旧容量oldCap
* 并且 如果底层数组queue还是目前的数组array,说明还没有线程成功扩容过,那么新建数组
* 这里的底层数组queue不是目前的数组array的情况是完全可能存在的:
* 第一个线程A在执行到tryGrow之中的if条件之前由于时间片到了而释放CPU的执行权,然后第二个线程B扩容成功,此时在queue被设置为线程B的newArray
* 随后第程A重新获取到CPU执行权之后,此时allocationSpinLock为0,尽管此时第二个线程已经完成扩容了,但是第一个线程仍然会执行CAS操作并且可以成功
* 因此线程A会进入if代码块,执行到这一步会发现此时底层数组queue不是目前的数组array,说明其他线程已经扩容了,那么线程A放弃操作
* 在下面的判断中将释放CPU的执行权,随后获取锁,并且由于newArray为null而直接返回
*/
if (newCap > oldCap && queue == array)
//那么新建数组,长度为新容量,然后赋给newArray
newArray = new Object[newCap];
} finally {
//无论此前是否发生异常,将标志位allocationSpinLock重置为0,同时只有一个线程执行到这里,因此不需要CAS操作就是线程安全的,后续的线程在CAS时就可能成功进入if代码块
allocationSpinLock = 0;
}
}
/*
* 到这一步,可能是:
* 线程将if代码块执行完毕,此时该扩容线程中的newArray不为null
* 或者 线程没有争取到扩容资格,没有执行if代码块,此时对应线程中的newArray为null
*/
//如果newArray为null,说明是没有执行if代码块的线程
if (newArray == null) // back off if another thread is allocating
/*
* 那么没有争取到扩容的线程尽量让出CPU的执行权,回到RUNNABLE状态,让自己和其他多个线程重新争夺cpu执行权。
* 其目的是让 正在扩容或者扩容成功的线程 先获取CPU的执行权向下执行,因为下一步就是获取锁的操作
* 但是yield方法并不一定会释放成功,毕竟java线程最终是调用操作系统的资源生成的,充满了不确定性。
*/
Thread.yield();
// 重新获取锁,根据上面的步骤,大概率是扩容成功的线程先获取到,但这不是绝对的
lock.lock();
//下面是获取锁之后的操作,是线程安全的,即串行化
/*
* 如果newArray不为null,说明成功执行了if代码块
* 并且 底层数组queue还是目前的数组array,说明还没有线程成功扩容过,那么在if代码块中为queue赋值,并转移数据
*
* 这里newArray不为null但是的底层数组queue不是目前的数组array的情况是完全可能存在的,有两种情况:
* 1 我们在前面说过,if代码块使用CAS控制同时只有一个线程能够进入if代码块中,但是不能保证
* 在一次扩容活动的过程中最终只有一条线程能够进入if代码块,因此可能有两条线程先后的进入if代码块成功初始化了newArray
* 这也是在上面先获取lock的原因,获取锁之后将里面的语句彻底串行化,同一时刻只有一条线程进入,因为即使有两条线程先后的进入if代码块成功初始化了newArray
* 但是只有一条线程能够将queue设置为newArray,后续的线程进来,虽然newArray不为null,但是此时queue却不等于array,因此不会再进行queue赋值操作
*
* 2 同样首先有一条线程A进入if代码块初始化了newArray,但是在这一步之前因为CPU时间片到了而停止执行。
* 随后另一条线程B同样进入if代码块初始化了newArray,并且为queue赋值新数组成功,随后新数组再次被填满,queue又被其他线程替换为新数组
* 假设此时A才获得CPU时间片,他会发现此时queue却不等于array,因为已经扩容了好多轮次了,因此不会再进行queue赋值操作
*/
if (newArray != null && queue == array) {
//那么底层数组queue赋值为新数组,
queue = newArray;
//将老数组中的数据转移到新数组对应的索引位置中,tryGrow方法结束
System.arraycopy(array, 0, newArray, 0, oldCap);
/*
* 到这一步才算真正的扩容成功
* 一次扩容过程中进入过if代码块 并且第一个获取锁的线程 才会走到这一步
* tryGrow方法同样结束,然后在外面的offer方法的下一次while循环中,会有如下情况:
* 由于扩容成功,queue=newArray,那么此时size会小于新数组的容量,此时就可以结束while循环
*/
}
/*
* 到这一步,没有执行if代码块的线程 或者 进入过if代码块但是和别的线程扩容冲突的线程 都将会走到这一步
* tryGrow方法同样结束,然后在外面的offer方法的下一次while循环中,会有如下情况:
* 1 其他线程还没有扩容成功,此时size还是大于等于底层数组的容量,然后又会进入该方法,尝试竞争扩容
* 2 其他线程扩容成功,此时size会小于新数组的容量,此时就可以结束while循环
* 3 其他线程已经扩容成功,但是扩容成功之后的数组又被被填满了,此时size还是大于等于底层数组的容量,然后又会进入该方法,这时该线程是在尝试另一次新的扩容
*/
}
根据源码分析,tryGrow方法可以分为两大步骤:
- 释放lock锁,然后如果CAS将allocationSpinLock从0变成1成功,那么尝试建立新数组newArray或者容量超限直接抛出OutOfMemoryError异常;CAS失败的线程可能尽量会交出CPU执行权,让CAS成功的线程向下执行。
- 重新获取lock锁,如果newArray不为null并且保存底层数组的全局变量queue指向的数组还等于此线程保存的旧数组array,那么为queue赋值为newArray,然后转移数据,此时扩容才算真正成功。
由此产生下面三个问题:
1、为什么在第一步尝试新建数组的时候要释放锁?不释放锁可以吗?
实际上不释放锁是完全可行的,并且代码会更简单,但是实际上在计算新容量、判断、新建数组的过程中,是没有加锁的必要的,因为这些计算和底层数组无关、和共享数据无关,如果此时加上了锁,那么在这个过程中的其他的操作,比如入队和出队操作由于获取不到锁也是不能执行的,这样在一定程度上降低了并发度。
2、为什么只有CAS成功的线程才能执行新数组的初始化操作呢?不采用CAS可以吗?
实际上,这里不采用CAS也完全没有问题,因为我们在后续加锁之后的代码中,会判断queue == 1 array,这表示只有还没有扩容成功时,才能为queue赋值。
在一次扩容过程中,就算有多个线程初始化了newArray,但是最终仍然只有一个线程能够为queue赋值,后续的线程获取锁之后会发现此时queue已经不等于array了,因此也不会出问题。
但是我们会发现,这种情况下,每一条数组都会进行if代码块中的操作,包括计算新容量、判断、新建数组,甚至抛出异常操作,这样的话实际上对每一条线程都降低了效率并且极大的浪费了内存空间。而我们使用CAS之后,同一时刻只有一条线程进入if代码块,其他线程非常有可能在下面的代码中释放CPU执行权给进入if代码块的线程让步,虽然在扩容活动中仍有可能有多条线程进入if代码块,但由于不会同时刻进入,导致进入if代码块的线程数量非常少,能明显能提升单个效率和减少空间浪费。
这里的情况也说明即使在获取锁之后,在为queue赋值之前,校验queue == array也是很有必要的!
3、为什么要在获取lock锁之后才进行queue的赋值?为什么赋值之前需要检查queue == array,为什么在之后才进行数据的转移?
在第二个问题中我们就说过,在扩容活动中使用CAS可以使得同一时刻只有一条线程进入if代码块,但是不同的时刻(比如一条线程执行if代码块完毕,那么此时另一条线程可以进入if代码块)则可以有不同的线程进入,因此对于最终queue的赋值还是需要加锁并且配合newArray != null && queue == array来判断。
而数据的转移也在此后进行的原因则很简单,因为此前没有加锁的时候,可能有线程执行了出队列操作,而在转移数据的时候,需要保证最新数据的一致性,因此需要加锁,lock锁还能保证最新数组元素的可见性
总结:
PriorityBlockingQueue的扩容机制,并没有整体加锁。对于和共享数据无关的新容量、判断、新建数组等操作没有加锁,只是使用了CAS限制了可以执行此步骤的线程数量。没加锁的好处就是在建立新数组的过程中,其他线程可以获取锁进行自己的操作,比如入队、出队等操作;而对于queue的赋值时则加了锁和判断,又保证最终扩容的正确性。这样的操作进一步提升了并发量,但是说实话确实也提升了我们这些普通程序员理解源码的难度!::>_<::
2.3.1.2 siftUpComparable/siftUpUsingComparator根据新结点构建小顶堆
offer添加元素的时候,需要根据“新结点”构建小顶堆,siftUpComparable和siftUpUsingComparator方法就是封装了这部分代码的逻辑,不同于根据数组构建小顶堆的方式,这里是向上递归查找构建,而根据数组构建小顶堆的siftDownComparable和siftDownUsingComparator方法是向下递归查找构建。siftUpComparable使用自然顺序比较,而siftUpUsingComparator使用指定比较器比较!
大概原理是:每添加一个元素,则将其与父结点进行比较,如果新添加结点大于等于父结点,则添加元素到该位置;否则,继续向上寻找父结点,直到找到某个位置,使得位于该位置的新元素的值大于等于对应父结点的元素的值,并且将原位置上的元素一一向后挪动。
具体的原理分析我们在数据结构—堆(Heap)的原理介绍以及Java代码的完全实现的添加元素部分有详细讲解了,在此不做赘述!
/**
* 每添加一个元素,则将其与父结点进行比较,如果新添加结点大于等于父结点,则添加元素到该位置;
* 否则,继续向上寻找父结点,直到找到某个位置,使得位于该位置的新元素的值大于等于对应父结点的元素的值,并且将原位置上的元素一一向后(下层)挪动。
*
* @param k 存放元素的索引位置
* @param x 指定元素
* @param array 数组
*/
private static <T> void siftUpComparable(int k, T x, Object[] array) {
//x强转为Comparable类型,使用key保存
Comparable<? super T> key = (Comparable<? super T>) x;
/*
* 循环,如果k大于0,表示还存在父结点
* 寻找合适的位置k:在某个插入的位置的新结点大于等于对应的父结点的值
*/
while (k > 0) {
//根据完全二叉树规律,获取k结点的父结点索引
int parent = (k - 1) >>> 1;
//获取父结点值e
Object e = array[parent];
//如果key大于等于父结点e,那么结束循环
if (key.compareTo((T) e) >= 0)
break;
//如果key小于父结点e,那么不符合小顶堆的规律
//k的位置置为e,即原父结点e降低一层
array[k] = e;
//k设置为parent,即向上递归,下一次将会用 这一次的parent 和 parent的parent 作比较
k = parent;
}
//到这里,可能是:
//1 k=0,即第一次添加元素
//2 找到了真正的位置k,在该位置插入的新结点e大于等于对应的父结点的值,然后插入元素key
array[k] = key;
}
/**
* 每添加一个元素,则将其与父结点进行比较,如果新添加结点大于等于父结点,则添加元素到该位置;
* 否则,继续向上寻找父结点,直到找到某个位置,使得位于该位置的新元素的值大于等于对应父结点的元素的值,并且将原位置上的元素一一向后(下层)挪动。
*
* @param k 存放元素的索引位置
* @param x 指定元素
* @param array 数组
* @param cmp 指定比较器
*/
private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
Comparator<? super T> cmp) {
/*
* 循环,如果k大于0,表示还存在父结点
* 寻找合适的位置k:在某个插入的位置的新结点大于等于对应的父结点的值
*/
while (k > 0) {
//根据完全二叉树规律,获取k结点的父结点索引
int parent = (k - 1) >>> 1;
//获取父结点值e
Object e = array[parent];
//如果x大于等于父结点e,那么结束循环
if (cmp.compare(x, (T) e) >= 0)
break;
//如果key小于父结点e,那么不符合小顶堆的规律
//k的位置置为e,即原父结点e降低一层
array[k] = e;
//k设置为parent,即向上递归,下一次将会用 这一次的parent 和 parent的parent 作比较
k = parent;
}
//到这里,可能是:
//1 k=0,即第一次添加元素
//2 找到了真正的位置k,在该位置插入的新结点e大于等于对应的父结点的值,然后插入元素key
array[k] = x;
}
2.3.2 put(e)方法
public void put(E e)
将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞。
这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用put方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!
如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较,那么抛出ClassCastException;如果指定元素为 null,那么抛出NullPointerException。
内部实际上就是调用的offer(e)方法!
/**
* 将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,没有返回值
*
* @param e 指定元素
* @throws ClassCastException 如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较
* @throws NullPointerException 如果指定元素为 null
*/
public void put(E e) {
//直接调用offer方法
offer(e); // never need to block
}
2.3.3 offer(e, timeout, unit)方法
public boolean offer(E e, long timeout, TimeUnit unit)
将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,应该忽略timeout 和unit参数,这个方法只为了兼容父接口BlockingQueue的同名抽象方法!
这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用offer方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!
如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较,那么抛出ClassCastException;如果指定元素为 null,那么抛出NullPointerException。
内部实际上就是调用的offer(e)方法!
/**
* 将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,应该忽略timeout 和unit参数。
*
* @param e 指定元素
* @param timeout 忽略此参数,因为此方法不会阻塞
* @param unit 忽略此参数,因为此方法不会阻塞
* @return true
* @throws ClassCastException 如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较
* @throws NullPointerException 如果指定元素为 null
*/
public boolean offer(E e, long timeout, TimeUnit unit) {
//直接调用offer方法
return offer(e); // never need to block
}
2.3.4 add(e)方法
public boolean add(E e)
将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞。
这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用add方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!
如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较,那么抛出ClassCastException;如果指定元素为 null,那么抛出NullPointerException。
内部实际上就是调用的offer(e)方法!
/**
* 将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞。
*
* @param e 指定元素
* @return true
* @throws ClassCastException 如果根据优先级队列的排序规则无法将指定元素与优先级队列中当前的元素进行比较
* @throws NullPointerException 如果指定元素为 null
*/
public boolean add(E e) {
//直接调用offer方法
return offer(e);
}
2.4 出队操作
虽然是无界队列,但是某些方法在出队的时候仍然需要判断队列是否为null,如果为null将可能被阻塞。
2.4.1 take()方法
public E take()
获取并移除此队列的头部,在元素变得可用(队列非空)之前一直等待。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
大概步骤为:
- 可中断的等待获取lock锁,即响应中断;没有获取锁则阻塞在同步队列中,如果被中断则抛出异常并返回;获取锁之后进入下一步;
- 开启一个循环,调用dequeue方法获取并移除此队列的头部,并重构小顶堆,队列为空就返回null;如果返回null,那么需要在notEmpty上等待;被唤醒之后继续循环;
- 移除队头完毕之后,在finally中释放锁;
- 如果前面没有发生异常,则返回被移除的移除此队列的头。
/**
1. @return 获取并移除此队列的头部,在元素变得可用(队列非空)之前一直等待。
2. @throws InterruptedException 如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,
3. 如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//可中断的等待获取lock锁,即响应中断
lock.lockInterruptibly();
//保存被移除的移除此队列的头部元素
E result;
try {
/*
* 开启一个循环
* 调用dequeue方法,获取并移除此队列的头部,并重构小顶堆,队列为空就返回null
* 如果返回的值为null,那么说明队列空了,需要在notEmpty上等待;被唤醒之后继续循环
*/
while ((result = dequeue()) == null)
notEmpty.await();
} finally {
//释放锁
lock.unlock();
}
//返回被移除的移除此队列的头
return result;
}
2.4.1.1 dequeue移除堆头并重构小顶堆
这一部分和10种常见排序算法原理详解以及Java代码的完全实现—堆排序中的重构小顶堆部分以及PriorityBlockingQueue构造器中的对某结点向下构建部分小顶堆是一致的:实际上就是将需要删除的元素(即根结点,也是数组头部结点)与堆尾元素(数组尾部结点)互换,之后从被删除元素的索引处开始向下重构小顶堆的过程,只不过这里需要移除堆尾元素(置空)。
大概步骤为:
- 初始化n= size-1 ,表示移除头部之后的队列(堆)大小;
- 如果n小于0,表示此时队列(堆)没有元素,直接返回null;
- 否则,获取数组头部元素result,这就是需要被移除的队列头,也是小顶堆的根结点;获取真正被移除元素x,就是队列尾部(堆尾),就是数组n索引位置的元素,同时保存尾部元素x。
- 数组n索引位置的元素即堆尾置为null,获取比较器cmp,如果cmp为null,调用siftDownComparable对某结点向下构建部分小顶堆,使用自然排序;如果cmp不为null,调用siftDownUsingComparator对某结点向下构建部分小顶堆,使用自然排序。前两个参数传入0,x,表示将堆的根结点看作x;后面传入的堆大小为n,即size-1,说明堆减少了一个元素,就是尾部。这里的意思是将x的元素逻辑移动至队列头部,暂时成为根结点,然后由根结点向下构建小顶堆,在构造器部分就是调用这个方法。
/**
* @return 获取并移除此队列的头部,并重构小顶堆,队列为空就返回null
*/
private E dequeue() {
//n为 size-1 ,表示移除头部之后的队列(堆)大小
int n = size - 1;
//如果n小于0,表示此时队列(堆)没有元素
if (n < 0)
return null;
else {
//获取底层数组
Object[] array = queue;
//获取数组头部元素,这就是需要被移除的队列头,也是小顶堆的根结点
E result = (E) array[0];
//获取真正被移除元素x,就是队列尾部(堆尾),同时保存尾部元素x
E x = (E) array[n];
//索引n的位置置空
array[n] = null;
//获取比较器
Comparator<? super E> cmp = comparator;
//如果cmp为null
if (cmp == null)
/*
* 调用siftDownComparable对某结点向下构建部分小顶堆,使用自然排序
* 前两个参数传入0,x,表示将堆的根结点看作x;后面传入的堆大小为n,即size-1,说明堆减少了一个元素,就是尾部。
* 这里的意思是将x的元素逻辑移动至队列头部,暂时成为根结点,然后由根结点向下构建小顶堆
* 在构造器部分就是调用这个方法
*/
siftDownComparable(0, x, array, n);
else
/*
* 调用siftDownUsingComparator对某结点向下构建部分小顶堆,使用自然排序
* 前两个参数传入0,x,表示将堆的根结点看作x;后面传入的堆大小为n,即size-1,说明堆减少了一个元素,就是尾部。
* 这里的意思是将x的元素逻辑移动至队列头部,暂时成为根结点,然后由根结点向下构建小顶堆
* 在构造器部分就是调用这个方法
*/
siftDownUsingComparator(0, x, array, n, cmp);
//size置为n,减少了1
size = n;
//返回result
return result;
}
}
2.4.2 poll()方法
public E poll()
获取并移除此队列的头,如果此队列为空,则直接返回 null。
相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。
/**
* @return 获取并移除此队列的头,如果此队列为空,则直接返回 null。
*/
public E poll() {
final ReentrantLock lock = this.lock;
//不可中断的等待获取锁,即不响应中断
lock.lock();
try {
//调用一次dequeue方法,并且返回 返回值即可
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
2.4.3 poll(timeout, unit)方法
public E poll(long timeout, TimeUnit unit)
获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。返回此队列的头部;如果在元素可用前超过了指定的等待时间,则返回 null。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
/**
* 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。
*
* @param timeout 超时时间
* @param unit 时间单位
* @return 返回此队列的头部;如果在元素可用前超过了指定的等待时间,则返回 null。
* @throws InterruptedException
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//计算超时时间纳秒
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//可中断的等待获取锁,即响应中断
lock.lockInterruptibly();
E result;
try {
/*
* 开启一个循环
* 调用dequeue方法,获取并移除此队列的头部,并重构小顶堆,队列为空就返回null
* 如果返回的值为null,那么说明队列空了,并且如果剩余等待超时时间大于0,那么需要在notEmpty上等待nanos时间,awaitNanos将返回剩余等待超时时间
* awaitNanos返回之后进入下一次循环
* 下一次循环时,如果此时result不为null那么说明成功出队列,结束循环;如果result为null,但是nanos小于等于0,那么表示超时时间到了也没有成功出队列,同样结束循环
*/
while ((result = dequeue()) == null && nanos > 0)
//最多超时等待nanos,中途返回的时候返回剩余超时等待时间
nanos = notEmpty.awaitNanos(nanos);
} finally {
//释放锁
lock.unlock();
}
//返回result
return result;
}
2.4.4 remove()方法
public E remove()
获取并移除此队列的头。此方法与 poll 唯一的不同在于此队列为空时将抛出一个NoSuchElementException异常。
相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。
内部实际上就是调用的poll方法,根据poll方法的返回值判断是否需要抛出异常!
/**
* 获取并移除此队列的头。此方法与 poll 唯一的不同在于此队列为空时将抛出一个NoSuchElementException异常。
*
* @return 队列头
* @throws NoSuchElementException 此队列为空
*/
public E remove() {
//直接调用poll方法,获取返回值x
E x = poll();
//如果x不为null,那么返回x;否则抛出NoSuchElementException异常
if (x != null)
return x;
else
throw new NoSuchElementException();
}
2.4.5 remove(o)方法
public boolean remove(Object o)
从此队列中移除指定元素的单个实例(如果存在)。如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。
大概步骤为:
- 不可中断的等待获取锁,即不响应中断;没有获取锁则阻塞在同步队列中;获取锁之后进入下一步;
- 获取锁之后调用indexOf方法在数组中从头开始查找与指定元素o相等(equals)的元素首次出现的位置,返回i;
- 如果i为-1,说明o为null或者没找到,直接返回false,释放锁,方法结束;
- 否则,调用removeAt移除指定索引位置i的元素,并重构小顶堆。返回true,释放锁,方法结束;
/**
* 从此队列中移除指定元素的单个实例(如果存在)。
*
* @param o 指定元素
* @return 如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。
*/
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
//不可中断的等待获取锁,即不响应中断
lock.lock();
try {
//在数组中查找o首次出现的索引位置,o为null或者没找到就返回-1
int i = indexOf(o);
//如果返回-1
if (i == -1)
//直接返回falsee
return false;
//移除指定索引位置i的元素,并从该位置开始向下重构小顶堆
removeAt(i);
return true;
} finally {
lock.unlock();
}
}
/**
1. 在数组中查找o首次出现的索引位置
2. 3. @param o 指定元素
4. @return o首次出现的索引位置,o为null或者没找到就返回-1
*/
private int indexOf(Object o) {
//如果o不为null,进入if代码块
if (o != null) {
Object[] array = queue;
int n = size;
//循环遍历数组,从0索引开始向后查找与0相等的元素首次出现的索引位置并返回
for (int i = 0; i < n; i++)
if (o.equals(array[i]))
return i;
}
//o为null或者没找到,都返回-1
return -1;
}
2.4.5.1 removeAt指定移除并重构小顶堆
removeAt用于移除指定索引位置的结点,并重构小顶堆,和数据结构—堆(Heap)的原理介绍以及Java代码的完全实现中的指定元素移除差不多,大概有两步:
- 第一步和dequeue方法差不多,将需要删除的元素与堆尾元素互换,移除堆尾元素,之后从被删除元素的索引处开始向下重构小顶堆。主要调用siftDownComparable或者siftDownUsingComparator方法!
- 向下重构小顶堆的操作只能保证从该索引位置开始下面的结构满足堆的要求。removeAt的向下重构和take、poll等方法中的向下重构不同的是,take、poll等方法是从堆顶部开始重构的,因此能够保证整个堆的满足要求,而removeAt却可能从堆的中间的某个结点开始的,在向下重构完毕之后,还需要校验起始索引位置的结点有没有调整位置,因为最开始就是一个小顶堆结构,即父结点小于等于子结点。如果调整了位置,那么说明起始索引之前的结构也满足要求;如果没有调整位置,那么可能出现起始位置的元素不仅小于等于其子结点,还可能小于其父结点的情况,此时还需要一次从该位置开始的向上的重构小顶堆来保证从该索引位置开始上面的结构也满足堆的要求!因此还可能会调用siftUpComparable或者siftUpUsingComparator方法!
/**
* 移除指定索引位置的元素,并从该位置开始重构小顶堆
*
* @param i 索引位置
*/
private void removeAt(int i) {
Object[] array = queue;
//n为 size-1 ,表示移除一个元素之后的队列(堆)大小
int n = size - 1;
/*如果移除的元素就是堆尾部元素,那么不需要重构小顶堆*/
if (n == i) // removed last element
//直接将堆尾的位置置空即可
array[i] = null;
/*否则,需要重构小顶堆*/
else {
/*
* 这里的重构和take、poll、remove等移除队头的方法是一样的
* 实际上就是将需要删除的元素与堆尾元素互换,然后移除堆尾元素,之后从被删除元素的索引处开始重构大顶堆的过程。
*/
//保存堆尾元素
E moved = (E) array[n];
//堆尾置空
array[n] = null;
//获取比较器
Comparator<? super E> cmp = comparator;
//如果cmp为null
if (cmp == null)
/*
* 调用siftDownComparable对某结点向下构建部分小顶堆,使用自然排序
* 前两个参数传入i,moved,表示将堆的i位置的结点看作moved;后面传入的堆大小为n,即size-1,说明堆减少了一个元素,就是尾部。
* 这里的意思是将x的元素逻辑移动至被删除元素的位置,然后由该位置向下构建小顶堆
* 在构造器部分就是调用这个方法
*/
siftDownComparable(i, moved, array, n);
else
/*
* 调用siftDownUsingComparator对某结点向下构建部分小顶堆,使用指定比较器排序
* 前两个参数传入i,moved,表示将堆的i位置的结点看作moved;后面传入的堆大小为n,即size-1,说明堆减少了一个元素,就是尾部。
* 这里的意思是将x的元素逻辑移动至被删除元素的位置,然后由该位置向下构建小顶堆
* 在构造器部分就是调用这个方法
*/
siftDownUsingComparator(i, moved, array, n, cmp);
/*
* 向下构建小顶堆的操作只能保证从该索引位置开始下面的结构满足堆的要求
* 构建之后如果i位置的元素就是moved,说明没有调整堆结构,那么可能出现被交换的元素不仅小于等于其子结点,还可能小于其父结点的情况
* 那么将该位置的元素看成新插入的元素,调用一次从该位置开始的向上构建小顶堆来保证从该索引位置开始上面的结构也满足堆的要求
*/
if (array[i] == moved) {
//如果cmp为null
if (cmp == null)
//调用siftUpComparable从i位置向上构建小顶堆,使用自然排序
siftUpComparable(i, moved, array);
else
//调用siftUpUsingComparator从i位置向上构建小顶堆,使用指定比较器排序
siftUpUsingComparator(i, moved, array, cmp);
}
}
//最后size赋值为n
size = n;
}
2.5 检查操作
2.5.1 peek()方法
public E peek()
获取但不移除此队列的头;如果此队列为空,则返回 null。
/**
* @return 获取但不移除此队列的头;如果此队列为空,则返回 null。
*/
public E peek() {
final ReentrantLock lock = this.lock;
//不可中断的等待获取消费者锁,即不响应中断
lock.lock();
try {
//判断size是否为0,即队列是否为空,如果是那么返回null,否则返回数组第一个节点,那就是队列的头,也是堆的根结点,也是最小的结点
return (size == 0) ? null : (E) queue[0];
} finally {
//释放锁
lock.unlock();
}
}
2.5.2 element()方法
public E element()
获取但是不移除此队列的头。此方法与 peek 唯一的不同在于此队列为空时将抛出一个异常。
/**
* 获取,但是不移除此队列的头。此方法与 peek 唯一的不同在于:此队列为空时将抛出一个异常。
*
* @return 队头
* @throws NoSuchElementException 如果此队列为空
*/
public E element() {
//内部调用peek方法获取返回值x
E x = peek();
//如果x不为null,那么返回x;否则抛出NoSuchElementException异常
if (x != null)
return x;
else
throw new NoSuchElementException();
}
2.6 size操作
public int size()
返回此队列中元素的数量。这个方法加了锁,因此返回的是精确值!
public boolean isEmpty()
如果此队列不包含元素,则返回 true。
/**
* @return 返回此队列中元素的数量
*/
public int size() {
final ReentrantLock lock = this.lock;
//不可中断的等待获取lock锁,即不响应中断
lock.lock();
try {
//返回size
return size;
} finally {
//释放锁
lock.unlock();
}
}
/**
* 直接判断size()方法是否返回0
*/
public boolean isEmpty() {
return size() == 0;
}
2.7 迭代操作
public Iterator< E > iterator()
返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator。和其他并发容器一样,返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,即支持并发修改,但是不保证迭代获取的元素就是此时队列中的元素!
下面的源码解析包括了迭代器的实现和方法,还是很简单的。弱一致性的原理在注释中也讲的很清楚,实际上需要迭代的底层数组副本在创建迭代器实例时就被存储了起来,后续就是对这个副本数组进行迭代,自然和原来的数组没有了太大关系,但是数组里面的元素只是浅克隆而已,因此迭代器的remove方法使用==比较才可能实现!
iterator(toArray)、remove方法调用时都需要加锁,因此会影响并发效率!
/**
* @return 返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator。
* 返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,即支持并发修改,
* 但是不保证迭代获取的元素就是此时队列中的元素!
*/
public Iterator<E> iterator() {
//返回一个Itr对象,传入toArray方法的返回值
return new Itr(toArray());
}
/**
* 返回包含此队列所有元素的数组。所返回数组的元素没有特定的顺序。
*
* @return 包含此队列所有元素的数组,相当于与元素的浅克隆
*/
public Object[] toArray() {
final ReentrantLock lock = this.lock;
//不可中断的等待获取lock锁,即不响应中断
lock.lock();
try {
//拷贝queue数组的前面[0,size)部分元素,到一个新数组,元素浅克隆
return Arrays.copyOf(queue, size);
} finally {
//解锁
lock.unlock();
}
}
/**
* “弱一致”的迭代器的内部实现
*/
final class Itr implements Iterator<E> {
//存储的数组快照
final Object[] array; // Array of all elements
//要迭代的下一个元素的索引,初始为0
int cursor; // index of next element to return
//最后一次迭代的元素的索引,初始为-1,没有也是-1,用于辅助删除
int lastRet; // index of last element, or -1 if no such
/**
* 构造器
*
* @param array 底层数组副本
*/
Itr(Object[] array) {
//lastRet置为-1
lastRet = -1;
//array赋值
this.array = array;
}
/**
* 是否有下一个结点
*
* @return true 是 false 否
*/
public boolean hasNext() {
//返回cursor是否小于快照数组的长度
return cursor < array.length;
}
/**
* 返回下一个结点的值
* lastRet等于此时的current,同时计算下一个要返回的结点current和下一个要返回的值currentElement
*
* @return 下一个结点的值
*/
public E next() {
//如果current大于等于快照数组的长度,表示已经没有下一个结点了,直接抛出NoSuchElementException异常
if (cursor >= array.length)
throw new NoSuchElementException();
//lastRet等于此时的cursor,即最后一次迭代的元素索引
lastRet = cursor;
//返回底层数组cursor索引的值,然后cursor自增1
return (E) array[cursor++];
}
/**
* 移除上一次next方法返回的元素,即最后一次迭代的结点
*/
public void remove() {
//如果lastRet 为小于0,表示最后迭代的结点已经被移除了,或者还没有开始迭代,直接抛出IllegalStateException异常
if (lastRet < 0)
throw new IllegalStateException();
//调用removeEQ尝试在真正的底层数组queue中移除和array[lastRet]元素相等的找到的第一个元素
removeEQ(array[lastRet]);
//lastRet重置为-1,表示最后迭代的结点已经被移除了
lastRet = -1;
}
}
/**
* 位于PriorityBlockingQueue 中的方法
* 用于Itr.remove()方法调用,移除底层数组中找到的与指定元素o相等的第一个元素
* 使用 == 比较是否相等
*
* @param o 指定需要被移除的元素
*/
void removeEQ(Object o) {
final ReentrantLock lock = this.lock;
//不可中断的等待获取lock锁,即不响应中断
lock.lock();
try {
//获取目前的底层数组
Object[] array = queue;
//遍历数组元素,移除找到的第一个相等的元素
for (int i = 0, n = size; i < n; i++) {
//这里和remove(o)方法不一样,这里是使用 == 比较的,而remove(o)
//如果o等于目前的底层数组的某个位置的元素,这里的相等就是指的同一个对象
if (o == array[i]) {
//调用removeAt,从目前的底层数组中移除该索引位置的元素
removeAt(i);
//跳出循环
break;
}
}
} finally {
//解锁
lock.unlock();
}
}
3 PriorityBlockingQueue的案例
3.1 基本使用
public class PriorityBlockingQueueTes2 {
public static void main(String[] args) {
PriorityBlockingQueue<Object> objects = new PriorityBlockingQueue<>();
//放入元素
objects.put(new PriorityTask(1));
objects.put(new PriorityTask(10));
objects.put(new PriorityTask(11));
objects.put(new PriorityTask(2));
objects.put(new PriorityTask(0));
objects.put(new PriorityTask(9));
objects.put(new PriorityTask(20));
//取出元素
for (int i = 0; i < 7; i++) {
System.out.println(objects.poll());
}
}
/**
* 任务实现,同时作为Comparable的实现类
*/
static class PriorityTask implements Comparable<PriorityTask> {
private int priority;
PriorityTask(int priority) {
this.priority = priority;
}
/**
* 当前对象和其他对象做比较,当前priority大就返回-1,当前priority小就返回1,
*
* @param o 被比较的线程任务
* @return 返回-1就表示当前任务出队列优先级更高;返回1就表示当前任务出队列优先级更低;即priority值越大,出队列的优先级越高;
*/
@Override
public int compareTo(PriorityTask o) {
return this.priority > o.priority ? -1 : 1;
}
@Override
public String toString() {
return "PriorityTask{" +
"priority=" + priority +
'}';
}
}
}
3.2 优先任务队列
PriorityBlockingQueue更常见的是给一批线程任务排序,和线程任务结合使用!
public class PriorityBlockingQueueTest {
public static void main(String[] args) {
//一个线程池,使用优先级的PriorityBlockingQueue阻塞队列作为任务队列
ExecutorService pool = new ThreadPoolExecutor(0, 1, 1000, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
/*
* 循环从 priority=0开始添加任务,即最先添加的任务优先级最低
* 查看输出执行情况,可以发现最后添加的任务(优先级最高)最先被执行
*/
for (int i = 0; i < 20; i++) {
pool.execute(new ThreadTask(i));
}
pool.shutdown();
}
/**
* 任务实现,同时作为Comparable的实现类
*/
static class ThreadTask implements Runnable, Comparable<ThreadTask> {
private int priority;
ThreadTask(int priority) {
this.priority = priority;
}
/**
* 当前对象和其他对象做比较,当前priority大就返回-1,当前priority小就返回1,
*
* @param o 被比较的线程任务
* @return 返回-1就表示当前任务出队列优先级更高;返回1就表示当前任务出队列优先级更低;即priority值越大,出队列的优先级越高;
*/
@Override
public int compareTo(ThreadTask o) {
return this.priority > o.priority ? -1 : 1;
}
@Override
public void run() {
System.out.println("priority:" + this.priority + ",ThreadName:" + Thread.currentThread().getName());
}
}
}
4 PriorityBlockingQueue的总结
为什么具有优先级的任务队列要使用“堆”这种结构以及堆排序的部分算法?
实际上采用其他算法也不是不可以,但是堆排序是这里最适合的一种算法。首先,我们这里是一个队列结构,需求是每次出队列的都是优先级最高的任务,实际上就是获取最小的元素,至于其他元素怎么排列的和我们无关,外界也无从得知,另外每次的添加和删除都需要重新找到最小的元素,放在固定的位置,利于后面的出队操作。那么实际上,我们并不是追求所有元素整体有序,我们只追求“部分有序”即可,最要的是找到最小的元素。
然后我们来看其他排序算法,最常见和简单的冒泡排序、选择排序、插入排序、希尔排序由于时间复杂度过高,都是属于指数级别时间复杂度,因此直接不考虑。
而并归排序和快速排序,虽然时间复杂度和堆排序都是O(nlogn),但是它们需要额外的辅助空间,而堆排序则不需要,因此堆排序的空间复杂度更低。
堆排序分为两步:第一步是构建堆,第二步是循环排序重建堆,这两部分总体时间复杂度为O(nlogn)。但是完成第一部分构建堆的时候,我们就能够确定一批元素的最值了。而在PriorityBlockingQueue中,我们只存在构建堆(调用传入集合的构造器),以及后续方法的单次排序重建堆的操作。所以在PriorityBlockingQueue中,如果是调用传入集合的构造器,那么仅仅是构建堆即可找到最小的元素,时间复杂度可以降低至O(n),而并归排序和快速排序至少为O(nlogn);如果是后续单次插入、移除元素,排序重建堆以找到最小的元素操作的时间复杂度可以降低至O(logn)。更重要的就是对于并归排序和快速排序,每次插入元素之后想要找到最小的元素又要整体重新排序,需要至少O(nlogn)的时间复杂度,快速排序由于此时元素整体有序其时间复杂度为甚至可以达到O(n²)。
因此,根据PriorityBlockingQueue只关注一批元素中最小的元素的特性,选择堆这种数据结构以及部分堆排序的原理来实现优先级队列是再合适不过了!
总结:
PriorityBlockingQueue内部只有一个独占锁来实现线程安全,出队、入队、检查等操作都需要获得这一把锁,一定程度上降低并发度,改进就是在内部数组扩容的时候对于新数组的操作部分就没有加锁,而是采用了CAS,这样又提升了一定的并发度。
PriorityBlockingQueue内部只有一个notEmpty条件变量,因为PriorityBlockingQueue是无界阻塞队列,put时候不需要就能查队列是否已满,因此不需要阻塞和唤醒,但是poll的时候需要检查队列是否非空,因此notEmpty是用于消费线程的阻塞和唤醒的!
PriorityBlockingQueue底层是一个数组,存储的元素必是Comparable或者Comparator的实现类,必须能够比较,数组是可以自动扩容的!这里的所谓的“无界”队列实际上是“有界”的,内部元素个数肯定不能超过Integer.MAX_VALUE!
PriorityBlockingQueue优先级的实现是因为实现了一个逻辑上的小顶堆结构,这个小顶堆结构映射到数组中最鲜明的特征之一就是数组的头结点对应小顶堆的根结点。每次出队都是优先级最高的元素,实际上移除的就是数组头部元素,对应的就是最小堆根结点,即最小的元素。
在重写compareTo或者compare方法时,如果你希望元素的某些数据越大优先级越高,那么在比较的时候,可以使用被比较的元素减去比较的元素(比如上面案例中规定priority越大优先级越高,那么当比较元素的priority大于被比较元素的priority,就可以返回-1);如果你希望元素的某些数据越小优先级越高,那么可以正常比较。
阅读PriorityBlockingQueue的源码之前,最重要的是搞懂什么是“堆”结构,以及“小顶堆”的构建原理,如果我们明白了堆和构建原理,那么实际上PriorityBlockingQueue的源码还是挺简单的,否则如果不明白的堆结构的特性而直接看源码那么应该会有很多看不明白的地方!
相关文章:
- 堆排序:10种常见排序算法原理详解以及Java代码的完全实现,其中有关于堆排序的详解。
- 堆结构:数据结构—堆(Heap)的原理介绍以及Java代码的完全实现,其中有关于大顶堆和小顶堆的介绍和构建实现!
- ReentrantLock:JUC—ReentrantLock源码深度解析。
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!