图解PriorityBlockingQueue源码(java 8)

PriorityBlockingQueue 是一个带有优先级的阻塞队列。

基本原理和前面介绍的ArrayBlockingQueue类似。

在看这篇文章之前,你可以先仔细看下这篇文章——ArrayBlockingQueue源码解析

PriorityBlockingQueue和ArrayBlockingQueue一样,初始化时,可指定队列的大小。

不同的是,前者可以自动扩容,而后者不会。

另一个很大不同是,前者可以实现优先出队,后者则是先进先出

比如有这么一个场景,一个市场上陆陆续续来了很多卖苹果的。

这时来了一个大买家,说谁最便宜就买谁的。 先来后来无所谓。


    static class SaleApple implements Comparable<SaleApple> {
        int price; // 苹果的价格
        String brand; // 草果的品种
        SaleApple(int price, String brand){
            this.price = price;
            this.brand = brand;
        }

        @Override
        public int compareTo(SaleApple o) {
            return this.price - ((SaleApple) o).price;
        }
    }

    public static void main(String[] args) throws Exception {
        PriorityBlockingQueue<SaleApple> priorityQueue = new PriorityBlockingQueue<>(3);
        Thread t1 = new Thread(new Runnable() {

            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    SaleApple apple = new SaleApple(RandomUtil.randomInt(5,50), "one_brandNo" + i);
                    boolean offer = priorityQueue.offer(apple);
                    log.info("one_offer:{}, value:{}", offer, apple.price);
                }
            }
        }, "t1");

        t1.start();
        Thread.sleep(3000);
        SaleApple apple = priorityQueue.take();
        log.info(" price:{}, brand:{}", apple.price, apple.brand);
    }

上面是一个小demo,PriorityBlockingQueue 就特别适合这样的场景,出队的就是最便宜的那个。

PriorityBlockingQueue 底层是利用 PriorityQueue默认是小顶堆

堆这种数据结构如果不熟悉,可以看我之前的博客——数据结构:堆(小顶堆和大顶堆)

好,现在开始一点一点分析源码

一、初始化


    public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }
    

在本例中comparator是null, demo 中 SaleApple 已实现了Comparable接口。

    /**
     * 队列的默认初始容量 
     */
	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;

    /**
     * Condition对象
     */
    private final Condition notEmpty;

    /**
     * 扩容时用的CAS锁标记
     */
    private transient volatile int allocationSpinLock;

    /**
     * 优先级队列
     */
    private PriorityQueue<E> q;

初始化代码很简单,用一张图来形象说明下。

在这里插入图片描述

二、入队源码

    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); // 入队
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            notEmpty.signal(); // 唤醒出队操作
        } finally {
            lock.unlock(); // 释放锁
        }
        return true;
    }
    

代码很清晰,难点就是入队siftUpComparable(n, e, array)这个方法。

我先画图来说明入队的操作,然后再来看入队的源码

图上画的是一个简单的小顶堆。数据是放在下面那个数组中。

其中子节点与父节点,下标有这么一个关系。(子节点下标 - 1 )/2 = 父节点下标

比如数字8的下标是 5, 那 (5-1) / 2 = 2,就是其父节点的下标。

在这里插入图片描述

现在往这个堆中插入元素 1,

先将 1 放在最后一个位置,然后调整二叉树,直到其满足小顶堆的特性,即堆化

在这里插入图片描述
1放到数组最后,与父节点4比较大小,小于父节点,就交换位置,再进行下一轮比较

在这里插入图片描述
1与父节点2比较,小于父节点,与父节点位置进行交换。此时1是根节点,比较结束。

堆 这种数据结构,增删元素的时间复杂度是 O(logn) ,堆化的操作,保证堆结构没有被破坏。

理解了这几张图,再来看源码


    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        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; // 找到位置后赋值
    }
    

再看下扩容的源码 tryGrow(array, cap),扩容的条件是, size == queue.length


    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                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;
            }
        }
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        lock.lock();
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

先解锁,然后CAS 操作,allocationSpinLock 由0设置为1,成功则扩容。

原数组小于 64,那就扩容一倍, 否则扩容 50%。

最后再抢锁,将扩容后的数组覆盖原数组。

三、出队源码解析


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // 抢锁
        E result;
        try {
            while ( (result = dequeue()) == null)
                notEmpty.await(); // 队空,出队操作阻塞
        } finally {
            lock.unlock();
        }
        return result;
    }
    

notEmpty.await()notEmpty.signal() 这两个方法,之前的博文有详细说明。

这里不再讲了,如果不清楚的,看这篇——await 和 signal

dequeue() 过程,差不多就是入队操作反过来。

将堆顶元素去掉,然后,把数组最后一个放堆顶,再进行堆化。

还是看图吧,图容易理解。
在这里插入图片描述
1 出队,然后把最后一个元素4,放到 1 的位置,然后进行堆化

在这里插入图片描述
4 的两个子节点,取较小的 2。 4 大于 2,交换位置。此时,4 比其子节点小,堆化结束。


    private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0]; // 堆顶元素出队
            E x = (E) array[n]; // 取出数组中最后一个元素
            array[n] = null; // 将最后一个元素设置为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;
        }
    }

    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            // n 是元素总个数,n 后一半,是子节点,这个自己画几个二叉树算下就明白了。
            int half = n >>> 1;           
            while (k < half) { // k >= half 说明此时已是子节点,不用处理了
                int child = (k << 1) + 1; // 左子节点 的下标
                Object c = array[child]; // 左子节点的值
                int right = child + 1; // 右子节点的下标。
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    // 如果存在右子节点,并且右子节点小于左子节点
                    c = array[child = right]; // c 是取较小的那个子节点
                if (key.compareTo((T) c) <= 0) // 如果父节点比 c 小,停止堆化
                    break;
                array[k] = c; // 父节点比c大,那交换位置
                k = child; // 下标盯住调换后的位置
            }
            array[k] = key;
        }
    }

结合图,还有代码的注释,出队的源码就解析完了。

四、总结

PriorityBlockingQueue 是具有优先级的阻塞队列。初始容量是11,能自动扩容

默认是小顶堆,出队永远是最小值,反之大顶堆,出队的是最大值。

ArrayBlockingQueue 相比,ArrayBlockingQueue 是先进先出,初始化需要指定容量无法自动扩容

LinkedBlockingQueue相比,LinkedBlockingQueue 是 先进先出,入队出锁是两把不同的两把锁,底层是链表,不存在所谓的扩容不扩容。

这三个阻塞队列,都采用notEmpty.await()notEmpty.signal() 来阻塞与唤醒。

相关文章:

ArrayBlockingQueue源码解析

LinkedBlockingQueue源码解析

SynchronousQueue 源码解析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值