【Java并发编程】阻塞队列PriorityBlockingQueue实现原理及源码解析

        本篇我们将分析阻塞队列PriorityBlockingQueue实现原理,该阻塞队列每次取出的都是最小的对象,可以满足一定的实际场景。

        阻塞队列PriorityBlockingQueue从不阻塞写线程,而当队列元素为空时,会阻塞读线程的读取,当然也有非阻塞的方法(poll)。该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。

阻塞队PriorityBlockingQueue原理


1.写线程不阻塞,读线程在队列为空时阻塞

        当队列为满时,写线程不会阻塞,而会尝试去扩容,扩容成功就继续向阻塞队列写入数据。当队列为空时,读线程会阻塞等待,直到队列不为空,被写线程唤醒。因此该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。读写线程共用同一把独占锁。

阻塞队列

2.堆存储结构

        阻塞队列PriorityBlockingQueue每次取出的都是最小(或最大)的对象,而其底层的存储结构正是堆存储(ps:堆结构需要满足每一个父节点小于(或大于)每一个子节点),所以每次向阻塞队列添加的时候,需要根据元素大小,调整其在堆中的位置,注意不需要对所有元素排序,只对父子节点排序,只要满足堆性质即可。这样每次从阻塞队列中取出元素时,直接取出堆顶元素,即最小值(或最大值),然后重新调整堆。

 3.扩容机制

        当阻塞队列PriorityBlockingQueue满时,写线程会尝试扩容(扩增old+2或者old/2),注意,写线程扩容的时候会先释放独占锁,并获取一个扩容独占锁(此锁仅用于扩容,防止多个写线程同时扩容),这样做的目的是提高读线程的吞吐量。扩容的本质其实就是新建一个更大的object数组,然后把阻塞队列PriorityBlockingQueue中的原阻塞队列引用替换成新的数组。

4.PriorityBlockingQueue需要元素自然排序或者提供比较器

        前面说过,阻塞队列PriorityBlockingQueue使用的是堆存储结构,那么就需要对父子节点进行排序,所以需要阻塞队列PriorityBlockingQueue存储的元素实现了Comparable接口,或者在新建阻塞队列PriorityBlockingQueue的时候提供比较器。

PriorityBlockingQueue源码分析


        首先看一下,阻塞队列PriorityBlockingQueue的成员变量,分别代表什么含义。

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;

    //比较器,元素没有实现comparable接口时,需提供比较器
    private transient Comparator<? super E> comparator;

    //独占锁,读写线程共用这一把锁
    private final ReentrantLock lock;

    //读线程等待队列,写线程永远不会阻塞
    private final Condition notEmpty;

    //写线程扩容锁,通过CAS控制,只有一个写线程会将此变量从0变成1
    private transient volatile int allocationSpinLock;

       阻塞队列PriorityBlockingQueue添加元素的方法:

public void put(E e) {
        offer(e); // 从不需要阻塞。
    }
public boolean offer(E e) {
//1.不支持null值
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
//2.加锁
        lock.lock();
        int n, cap;
        Object[] array;
//3.如果队列满了,就调用tryGrow进行扩容
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> cmp = comparator;
//4.使用自然排序,将新元素添加到堆中。
            if (cmp == null)
                siftUpComparable(n, e, array);
//4.使用比较器,将新元素添加到堆中。
            else
                siftUpUsingComparator(n, e, array, cmp);
//5.队列中元素数量增1
            size = n + 1;
//6.唤醒读线程
            notEmpty.signal();
        } finally {
//7.释放锁
            lock.unlock();
        }
        return true;
    }

        从上面分析可知,当阻塞队列满了,写线程就会调用tryGrow方法对队列扩容,扩容成功后再向队列添加新元素。下面就看看扩容方法tryGrow:

private void tryGrow(Object[] array, int oldCap) {
//1.先释放锁,好让读线程继续读,提高吞吐量。
        lock.unlock(); 
//2.新阻塞队列
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
//3.获取扩容锁,通过CAS将allocationSpinLock从0变成1,仅有一个写线程获取该锁进行扩容
            try {
//4.扩容大小
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
//5.如果新容量大于MAX_ARRAY_SIZE,那么可能内存溢出
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
//6.如果旧容量已经到达最大值,那么此次扩容失败
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
//7.否则,就取最大容量MAX_ARRAY_SIZE
                    newCap = MAX_ARRAY_SIZE;
                }
//8.如果新容量大于旧容量并且原阻塞队列没有变(这里考虑了当扩容写线程释放扩容锁后,又有一个写
//线程过来扩容,那么不允许再次扩容)
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
//9.释放扩容锁
                allocationSpinLock = 0;
            }
        }
//10.如果有写线程正在扩容,就让出CPU等待
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
//11.再次获取独占锁
        lock.lock();
//12.再次判断是否是首个获取扩容所扩容的写线程,如果是就将阻塞队列引用为新队列。
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

添加元素到堆中的方法siftUpComparable,siftUpUsingComparator这两个一模一样,区别只是自然排序还是比较器排序。

//参考最上面阻塞队列原理2
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;
    }

        从阻塞队列PriorityBlockingQueue取出元素的方法:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
//1.可中断获取锁
        lock.lockInterruptibly();
        E result;
        try {
//2.如果阻塞队列为空,就阻塞等待
            while ( (result = dequeue()) == null)
                notEmpty.await();
        } finally {
//3.释放锁
            lock.unlock();
        }
        return result;
    }

下面看一下出队列的方法dequeue:

private E dequeue() {
        int n = size - 1;
//1.如果阻塞队列为空,返回null
        if (n < 0)
            return null;
        else {
//2.否则取出堆顶元素
            Object[] array = queue;
            E result = (E) array[0];
            E x = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
//3.重新调整堆
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

        总结: 阻塞队列PriorityBlockingQueue从不阻塞写线程,当队列满时,写线程会尝试扩容阻塞队列,扩容成功后再向阻塞队列中新增元素,而当队列元素为空时,会阻塞读线程的读取,当然也有非阻塞的方法(poll)。该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。阻塞队列采用堆存储结构,因此每次冲阻塞队列取出的元素总是最小元素(或最大元素)。而堆存储需要提供比较器或者元素实现了阻塞接口,否则程序会抛出ClassCastException。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值