java阻塞队列之PriorityBlockingQueue

PriorityBlockingQueue简介
它是一个数组实现的带优先级无阻塞队列并发安全队列。

PriorityBlockingQueue的特性
1.PriorityBlockingQueue内部是一个数组queue,但是其实数据结构是使用数组实现的一个最小堆,压入队列时需要计算最小堆,弹出队列时需要重新调整根节点。
2.带优先级的队列(带排序功能)。
3.无界队列,默认队列大小为11,当队列满了之后会自动扩容(和ArrayList类似的扩容数组)。
4.take队列为空时阻塞.但是PriorityBlockingQueue队列是无界的,put方法不存在队列满的时候阻塞的情况,所以put方法是不阻塞的,可以说PriorityBlockingQueue是一个半阻塞的队列
5.和ArrayBlockingQueue类似,是独占锁来控制的, 就是说多线程访问时只能有一个线程可以进行入队或出队操作。
6.PriorityBlockingQueue虽然是无界的,但是最大长度只能为Integer.MAX_VALUE - 8,并不是说无界了就可以任意长度,毕竟它是通过数组实现的,数组的最大值只能为Integer.MAX_VALUE。

入队put方法,内部调用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 {
            // 如果没有比较器,则元素e必须实现Comparable接口
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            // 此处唤醒调用take时队列为空的阻塞线程
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

队列满如何进行扩容?

    // 扩容的时候先释放锁,如果take的线程获取了锁可以取,如果offer的线程获取了锁可以放
    // (方法中释放了锁,别的线程就可以进去这个方法,也可以进去其它需要锁的方法)
    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // 释放锁,执行take的线程可以竞争锁,正常出队(保证性能)
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
                UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                        0, 1)) {// 此处使用cas,保证只有一个线程能进行扩容
            try {
                int newCap = oldCap + ((oldCap < 64) ?
                        (oldCap + 2) : // 如果队列容量低于64,则扩容后为2倍原容量+2
                        (oldCap >> 1));// 否则扩容为1.5倍原容量
                if (newCap - MAX_ARRAY_SIZE > 0) { // 这里对溢出的处理
                    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 {// 此处不需要cas操作,因为此处只有一个线程操作allocationSpinLock
                allocationSpinLock = 0;
            }
        }
        // 失败扩容的线程newArray == null,调用Thread.yield()让出cpu, 让扩容线程扩容后优先调用lock.lock重新获取锁,
        // 但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁。
        if (newArray == null)
            Thread.yield();
        lock.lock(); // 此处获取锁,可能是扩容线程,也可能是其它线程
        // 如果扩容线程获取到了锁,则能成功给共享变量赋值,如果不是,则根本进入不了下面的if代码块
        // 扩容线程newArray != null, 而其它线程newArray = null,至于其它线程为什么newArray为空,这是跟线程调用栈相关内容了。
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

入队元素调整(节点上浮)
siftUpComparable,siftUpUsingComparator这两个方法比较简单,简单说下之间的区别。
siftUpComparable方法添加的元素必须实现Comparable接口,通过元素自身的compareTo方法比较。
siftUpUsingComparator方法通过初始化时传入的comparator来比较。

private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

    @SuppressWarnings("unchecked")
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

构建最小堆过程
如果从一个优先级队列构造一个新的优先级队列,此时内部的数组元素不需要进行调整,只需要将原数组元素都复制过来即可。 (PriorityBlockingQueue(Collection<? extends E> c)构造器,如果传入的集合是SortedSet,PriorityBlockingQueue对象,不需要堆化操作) 但是从其它非PriorityQueue的集合中构造优先级队列时,需要先将元素复制过来后再进行调整,此时调用的是heapify方法。

    private void heapify() {
        // 从最后一个非叶子节点开始从下往上调整,最后非叶子节点获取下标(size >>> 1) - 1
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

    // 这个函数即对应上面的元素删除时从上往下调整的步骤
    private void siftDown(int k, E x) {
        if (comparator != null)
            // 如果比较器不为null,则使用比较器进行比较
            siftDownUsingComparator(k, x);
        else
            // 否则使用元素的compareTo方法进行比较
            siftDownComparable(k, x);
    }

    private void siftDownUsingComparator(int k, E x) {
        // 使用half记录队列size的一半,如果比half小的话,说明不是叶子节点
        // 因为最后一个节点的序号为size - 1,其父节点的序号为(size - 2) / 2或者(size - 3 ) / 2
        // 所以half所在位置刚好是第一个叶子节点
        int half = size >>> 1;
        while (k < half) {
            // 如果不是叶子节点,找出其孩子中较小的那个并用其替换
            int child = (k << 1) + 1; // 找出k下标所在节点的左子节点索引
            Object c = queue[child]; // 左子节点的值
            int right = child + 1; // 右子节点索引
            if (right < size &&
                    comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0) // 子节点较小的跟父节点比较
                break;
            queue[k] = c; // 替换索引k所在元素值
            k = child;
        }
        queue[k] = x;
    }
    // 同上,只是比较的时候使用的是元素的compareTo方法
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1; // 如果是非叶子节点则继续循环
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                    ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

siftDown方法是这里面比较重要的方法之一,有两个参数,一个是序号k,另一个是元素x,这个方法的作用是把x从k开始往下调整,使得在节点k在其子树的每相邻层中,父节点都小于其子节点。所以heapify的作用就比较明显了,从最后一个非叶子节点开始,从下往上依次调整其子树,使得最终得到的树里,根节点是最小的。这里要先理解一下为什么heapify中i的初始值要设置为(size >>> 1) - 1。因为这是最后一个非叶子节点的位置,不信的话可以随便画几个图验证一下,至于siftDownUsingComparator方法中,int half = size >>> 1;这里half则是第一个叶子节点的位置,小于这个序号的节点都是非叶子节点,结合下图来验证。

以下方数组为例

下标4的节点需要进行一轮调整,siftDown(4,queue[4]),此时while(k<half)中k=10,退出while循环。

下标1的节点需要进行两轮调整,siftDown(1,queue[1]),3-7互换,第二轮7小于左右子节点,直接结束。
下标0的节点需要进行三轮调整,siftDown(0,queue[0]),第一次14-3互换,此时while(k<half)中k=1,第二次14-6互换,此时while(k<half)中k=3,第三轮14-10互换,此时while(k<half)中k=8,退出while循环,最小堆构造完成。

移除最小堆指定元素
当移除的不是堆顶元素的时候,同样先用最后一个元素代替,然后先从被移除的位置开始向下调整,如果发现没有改动,说明子节点都比它大,则再向上调整。

// 这里不是移除堆顶元素,而是移除指定元素
    public boolean remove(Object o) {
        // 先找到该元素的位置
        int i = indexOf(o);
        if (i == -1)
            return false;
        else {
            removeAt(i);
            return true;
        }
    }
    // 移除指定序号的元素
    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        // s为最后一个元素的序号
        int s = --size;
        if (s == i) 
            queue[i] = null;
        else {
            // moved记录最后一个元素的值
            E moved = (E) queue[s];
            queue[s] = null;
            // 用最后一个元素代替要移除的元素,并向下进行调整
            siftDown(i, moved);
            // 如果向下调整后发现moved还在该位置,则再向上进行调整
            if (queue[i] == moved) { // 此时说明子节点都比它大
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

出队过程

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 加锁(可响应中断)
        lock.lockInterruptibly();
        E result;
        try {
            // 如果队列为空,take 方法会阻塞出队线程
            while ( (result = dequeue()) == null)
                /**
                 * 如果队列中没有元素,会阻塞后续调用 take 方法出队的线程
                 * 直到队列添加了元素后唤醒 notEmpty,才可以继续执行
                 */
                notEmpty.await();
        } finally {
            // 释放锁
            lock.unlock();
        }
        return result;
    }

    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];
            // 把最后一个元素置 null,因为要把它放到堆顶,向下逐步调整堆结构
            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;
        }
    }

参考:https://www.cnblogs.com/mfrank/p/9614520.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值