PriorityBlockingQueue
是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现。本身是线程安全的,内部使用显示锁ReentrantLock
一、PriorityBlockingQueue
类图结构
PriorityBlockingQueue
类中的成员变量与成员方法介绍:
queue
数组:用来存放队列元素size
:用来存放队列元素的个数allocationSpinLockOffset
:用来在扩容队列时候做cas,目的是保证只有一个线程可以进行扩容。comparator
:由于这是一个优先级队列,所以有一个比较器comparator用来比较元素的大小。lock
:独占锁对象用来控制同时只能由一个线程可以进行入队出队操作notEmpty
条件变量:用来实现take方法的阻塞模式- 这里没有
notFull
条件变量是因为这里的put操作是非阻塞的,为什么要设计为非阻塞的是因为这是无界队列。 PriorityQueue
:用来做序列化的。
二、使用该类的情况
一个典型的需求是:当你需要使用一个有序列表的数据结构的时候,java提供的PriorityBlockingQueue类就拥有这一种功能。
你想要添加到 PriorityBlockingQueue中的所有元素都必须实现Cmparable接口。这个接口有一个compareTo方法,它接收同样类型的对象,如有两个比较的对象:一个是执行这个方法的对象,另一个是作为参数接收的对象。如果本地对象小于参数,则该方法返回小于0的数值。如果本地对象大于参数,则该方法返回大于0的数值。如果本地对象等于参数,则该方法返回等于0的数值、
PriorityBlockingQueue使用 compareTo方法决定插入元素的位置。
三、构造函数
三、offer
操作
下面来看看上图中红色框圈起来主要的两个方法
从上图可以看出,在扩容的时候,首先要进行释放锁操作,释放锁的目的是想要使用cas来控制只有一个线程能够扩容成功,而不是使用全局锁,因为全局锁的粒度更大,使用全局锁后其他线程的入队出队都不能进行,这样会降低并发性。
扩容的大小有两种:
- 一种是如果旧的容量小于64,那么新的容量为:2 * 旧的容量 + 2,
- 如果旧的我拿过来大于64,那么新的容量为 1.5 * 旧的容量
上图中【第287行】 是获取锁,为什么在该方法的最开始要释放锁,在该方法的后面又要获取锁呢?这里是因为可见性的问题,因为queue没有被volatile修饰。另外可能在扩容的时候进行了出队操作,如果直接进行拷贝的话,可能看到的数组元素不是最新的,而通过调用 lock 后,获取的数组则是最新的,并且在释放锁之前数组的内容不会发生变化,因为其他线程不能进行入队出队操作。
poll
操作
extract方法的执行过程:
- 【步骤一】首先获取队列的头元素,放到变量
result
中 - 【步骤二】然后获取队列尾部元素,放到变量
x
中,并将队尾的元素置为null
- 【步骤三】根据默认的比较器或者用户提供的比较器来调整小根堆
假如当前队列元素:
对应的树结构为:
这个时候调用poll方法,那么对应的extract方法中【步骤一】中的result = 2, x = 11;然后在【步骤二】中,将队尾的元素置为null。所以这个时候树的结构如下:
我们根据源码来分析各个数值:
然后可以看到result= 2 的 leftChildVal = 4,rightChildVal = 6,n = 5, half = 2;因为 4 < 6 , 所以 c = 4,也就是获取根节点的左右孩子值最小的的哪一个;然后看11 > 4 ,也就是 key > c 所以【第389行】不成立,直接执行array[k] = c
。即把c放入树根,现在的树为:
然后看根的左孩子4为根的字树,我们要为这个子树找到根节点。
leftChildVal = 8, rightChildVal = 10, 8 < 10, 所以c = 8,也就是获取根节点的左孩子值最小的,然后看11 > 8 , 所以【第389行】不成立,直接执行array[k] = c
。即把c放入树根,现在的树为:
这是 k = 3, half = 3,退出while循环,执行【第394行】,即代码 array[k] = key
; 这个时候的树为:
对应的队列为:
size
操作
因为size
操作,添加了独占锁ReentrantLock
,所以获取的队列中元素的个数是精确的。
总结
一、PriorityBlockingQueue
类是一个优先级队列,本身是线程安全的,内部使用显示锁ReentrantLock
来保证线程安全。PriorityBlockingQueue
存储的对象必须是实现了 Comparable
接口的,因为PriorityBlockingQueue
队列会根据内存存储的每一个元素的comparaTo
方法来比较每个元素的大小。这样在take
的时候会根据优先级,将优先级最小的取出。
二、PriorityBlockingQueue
类似于 ArrayBlockingQueue
,其内部都使用了一个独占锁来控制同时只有一个线程可以进行入队和出队的操作,另外PriorityBlockingQueue
只使用了 notEmpty
条件变量,而没有使用 notFull
变量,这是因为前者是无界队列,当put
的时候永远都不会处于await
。
三、PriorityBlockingQueue
始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素的个数 >= 最大容量的时候会通过算法进行扩容操作。这里的扩容是通过释放锁使用cas
来实现的。
扩容的大小有两种:
- 一种是如果旧的容量小于64,那么新的容量为:2 * 旧的容量 + 2,
- 如果旧的我拿过来大于64,那么新的容量为 1.5 * 旧的容量
四、PriorityBlockingQueue
类的offer
和 poll
方法在放入数据和取出数据的时候分别会进行小根堆的建立siftUpComparable
,和小根堆的调整siftDownComparable
。