JUC源码分析-PriorityBlockingQueue

坚强是说给别人听的谎言 提交于 2020-02-25 18:10:03

概述

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借助二叉堆怎么实现排序的。

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!