Java并发编程之PriorityBlockingQueue阻塞队列详解

简介

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。

PriorityBlockingQueue有四个构造方法:

// 默认的构造方法,该方法会调用this(DEFAULT_INITIAL_CAPACITY, null),即默认的容量是11
public PriorityBlockingQueue()
// 根据initialCapacity来设置队列的初始容量
public PriorityBlockingQueue(int initialCapacity)
// 根据initialCapacity来设置队列的初始容量,并根据comparator对象来对数据进行排序
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator)
// 根据集合来创建队列
public PriorityBlockingQueue(Collection<? extends E> c)

PriorityBlockingQueue源码详解

PriorityBlockingQueue类定义为:

public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable

该类同样继承了AbstractQueue抽象类并实现了BlockingQueue接口,这里不再叙述。

PriorityBlockingQueue内部是采用二叉堆来实现的,这里不再解释,同时,该类使用ReentrantLock和Condition来确保多线程环境下的同步问题。

// 默认容量
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;
// 自旋锁
private transient volatile int allocationSpinLock;
// 为了兼容之前的版本,只有在序列化和反序列化才非空
private PriorityQueue<E> q;

我们前面介绍的ArrayBlockingQueue和LinkedBlockingQueue都是具有notEmpty和notFull两个Condition,那PriorityBlockingQueue为什么只有一个notEmpty呢?因为PriorityBlockingQueue是一个无界阻塞队列,可以一直向队列中插入元素,除非系统资源耗尽,所以该队列也就不需要notFull了。

入队

我们来看一下add(E e)方法:

public boolean add(E e) {
    return offer(e);
}

该方法很简单,其内部调用了offer(E e)方法:

public boolean offer(E e) {
    // 若插入的元素为null,则直接抛出NullPointerException异常
    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;
}

我们先看一看扩容操作:

private void tryGrow(Object[] array, int oldCap) {
    // 释放全局锁
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    // 使用CAS操作来修改allocationSpinLock
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                    0, 1)) {
        try {
            // 容量越小增长得越快,若容量小于64,则新容量是oldCap * 2 + 2,否则是oldCap * 1.5
            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;
        }
    }
    // newArray == null表明已经有其他线程对数组进行了扩容操作,让出CPU(因为扩容线程后续还有其他的操作)
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    // 获取全局独占锁
    lock.lock();
    // 若queue没有发生变化
    if (newArray != null && queue == array) {
        // 将创建的新数组赋值给queue
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

在扩容操作开始时,线程释放了全局锁,这是为什么呢?其实这也是为了更好的并发性,虽然释放了全局锁,但后面扩容操作是通过简单的乐观锁allocationSpinLock来进行控制的。

offer(E e)方法中还有两个比较重要的方法是siftUpComparable和siftUpUsingComparator,我们先来看一下siftUpComparable方法:

private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    // k是否达到二叉树的顶点
    while (k > 0) {
        // 计算父节点的下标
        int parent = (k - 1) >>> 1;
        Object e = array[parent];
        // key >= e说明已经按照升序排序,跳出循环
        if (key.compareTo((T) e) >= 0)
            break;
        // 否则,将parent节点的值放在子节点上
        array[k] = e;
        // 将parent当作下次比较的k
        k = parent;
    }
    // 将值key放置合适的位置上
    array[k] = key;
}

该方法的主要操作就是,将x放到位置k,然后调整x的位置,直到它大于等于父节点。

siftUpUsingComparator与siftUpComparable的唯一不同点在于,siftUpUsingComparator使用自定义的比较器来比较元素,其余操作相同。

出队

我们来看一下poll()方法,该方法每次都是返回数组下标为0的元素:

public E poll() {
    // 获取全局锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return dequeue();
    } finally {
        // 释放全局锁
        lock.unlock();
    }
}

出队操作其实是由dequeue()方法来完成的:

private E dequeue() {
    int n = size - 1;
    // 队列是否为空
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        // 取出第一个元素,作为返回值
        E result = (E) array[0];
        // 取出最后一个元素,然后将数组最后一个元素设置为null
        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;
    }
}

重新调整二叉树的操作是由siftDownComparable方法和siftDownUsingComparator方法来完成的,我们先看一下siftDownComparable方法:

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
        // k其实是half的父节点下标,若k >= half,则表明k在数组中已经没有子节点
        while (k < half) {
            // k的左子节点下标
            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];
            // key <= c说明已经按照升序排序,跳出循环
            if (key.compareTo((T) c) <= 0)
                break;
            // 否则,将子节点的值放在父节点上
            array[k] = c;
            // 将child当作下次比较的k
            k = child;
        }
        // 将值key放置合适的位置上
        array[k] = key;
    }
}

该方法的主要操作就是,将x放到位置k,然后调整x的位置,直到它小于等于子节点。

siftDownUsingComparator与siftDownComparable的唯一不同点在于,siftDownUsingComparator使用自定义的比较器来比较元素,其余操作相同。

相关博客

Java并发编程之ArrayBlockingQueue阻塞队列详解

Java并发编程之ReentrantLock详解

 Java并发编程之Condition详解

参考资料

方腾飞:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80586040