美文网首页java集合
java源码-PriorityBlockingQueue

java源码-PriorityBlockingQueue

作者: 晴天哥_王志 | 来源:发表于2018-08-06 16:11 被阅读54次

    开篇

     PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素是二叉树最小堆的实现。

     使用数组存储的时候i结点的父结点下标就为(i–1)/2。它的左右子结点下标分别为2*i+1和2*i+2

     堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:

    • Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2],即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。

    • 堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的,PriorityBlockingQueue是采用小顶堆实现的。

    类图

    PriorityBlockingQueue.png

    PriorityBlockingQueue构造器及相关变量

     PriorityBlockingQueue的相关类变量已经在下面注释了,构造函数核心的参数包括初始化容量大小和比较器comparator
     额外需要关注的是入参为Collection集合对象的时候,内部会区分是否有序,对于有序集合直接添加到数组queue当中,对于无序集合就需要在添加完成后的最后一步执行排序工作。heapify()方法就是执行这个排序的函数,后面请看分解。

    public class PriorityBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
        private static final long serialVersionUID = 5595510919245408276L;
          
        //初始化容量
        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;
    
        private transient volatile int allocationSpinLock;
    
    
        private PriorityQueue<E> q;
    
        public PriorityBlockingQueue() {
            this(DEFAULT_INITIAL_CAPACITY, null);
        }
    
    
        public PriorityBlockingQueue(int initialCapacity) {
            this(initialCapacity, null);
        }
    
    
        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];
        }
    
        public PriorityBlockingQueue(Collection<? extends E> c) {
            this.lock = new ReentrantLock();
            this.notEmpty = lock.newCondition();
            boolean heapify = true; // true if not known to be in heap order
            boolean screen = true;  // true if must screen for nulls
            if (c instanceof SortedSet<?>) {
                SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
                this.comparator = (Comparator<? super E>) ss.comparator();
                heapify = false;
            }
            else if (c instanceof PriorityBlockingQueue<?>) {
                PriorityBlockingQueue<? extends E> pq =
                    (PriorityBlockingQueue<? extends E>) c;
                this.comparator = (Comparator<? super E>) pq.comparator();
                screen = false;
                if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                    heapify = false;
            }
            Object[] a = c.toArray();
            int n = a.length;
            // If c.toArray incorrectly doesn't return Object[], copy it.
            if (a.getClass() != Object[].class)
                a = Arrays.copyOf(a, n, Object[].class);
            if (screen && (n == 1 || this.comparator != null)) {
                for (int i = 0; i < n; ++i)
                    if (a[i] == null)
                        throw new NullPointerException();
            }
            this.queue = a;
            this.size = n;
            if (heapify)
                heapify();
        }
    

    heapify过程说明

      heapify的整体逻辑就是一个堆排序过程,排序的对象是数组的0~(n/2-1)之间的元素。整个排序的核心逻辑就是父节点和左右子节点三者进行比较,三者当中最小的元素上浮。这个过程是从(n/2-1)的尾部元素开始到顶部元素进行排序的,所以我们可以理解为先保证底部元素有序后再逐步往顶部走。

        private void heapify() {
            Object[] array = queue;
            int n = size;
            int half = (n >>> 1) - 1;
            Comparator<? super E> cmp = comparator;
            if (cmp == null) {
                for (int i = half; i >= 0; i--)
                    siftDownComparable(i, (E) array[i], array, n);
            }
            else {
                for (int i = half; i >= 0; i--)
                    siftDownUsingComparator(i, (E) array[i], array, n, cmp);
            }
        }
    
    
        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
                while (k < half) {
                    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;
            }
        }
    }
    

    heapify图解说明

    针对下面数组说明最小堆的构建过程,初始化状态如下图。
    [7, 6, 5, 12, 10, 3, 1, 11, 15, 4 ]

    最小堆初始状态

      我们观察下用数组a建成的二叉堆,很明显,对于叶子节点4、15、11、1、3来说它们已经是一个合法的堆(这就是为啥是n/2-1)。所以只要最后一个节点的父节点,也就是最后一个非叶子节点a[4]=10开始调整,然后依次调整a[3]=12,a[2]=5,a[1]=6,a[0]=7,分别对这几个节点做一次"下移或者上浮"操作就可以完成了堆的构造。我们还是用图解来分析下这个过程。

    image image image

      整个调整过程如下:

      1. 对于节点a[4]=10的调整(图1),只需要交换元素10和其子节点4的位置(图2)。
      1. 对于节点a[3]=12的调整,只需要交换元素12和其最小子节点11的位置(图3)。
      1. 对于节点a[2]=5的调整,只需要交换元素5和其最小子节点1的位置(图4)。
      1. 对于节点a[1]=6的调整,只需要交换元素6和其最小子节点4的位置(图5)。
      1. 对于节点a[0]=7的调整,只需要交换元素7和其最小子节点1的位置,然后交换7和其最小自己点3的位置(图6)。

    PriorityBlockingQueue的添加过程

      PriorityBlockingQueue的添加过程就是两个过程

    • 添加元素到数组的最后一个位置
    • 通过siftUpComparable函数实现和父节点进行比较从而实现上浮直至满足最小堆排序。
    • 当然由于PriorityBlockingQueue是线程安全的,所以在底层的添加函数offer当中通过ReentrantLock的lock实现先锁后操作的流程。
        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);
                else
                    siftUpUsingComparator(n, e, array, cmp);
                size = n + 1;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
            return true;
        }
    
     
        public void put(E e) {
            offer(e); // never need to block
        }
    
     
        public boolean offer(E e, long timeout, TimeUnit unit) {
            return offer(e); // never need to block
        }
    

      siftUpComparablem函数的逻辑是递归的比较左字点、右节点、父节点三者之间的关系从而将最小元素进行上浮。

    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的添加过程图解

    PriorityBlockingQueue的添加过程图解-1 PriorityBlockingQueue的添加过程图解 -2

    结合上面的图解,我们来说明一下二叉堆的添加元素过程:

      1. 将元素2添加在最后一个位置(队尾)(图2)。
      1. 由于2比其父亲6要小,所以将元素2上移,交换2和6的位置(图3);
      1. 然后由于2比5小,继续将2上移,交换2和5的位置(图4),此时2大于其父亲(根节点)1,结束。

    PriorityBlockingQueue的删除过程

      PriorityBlockingQueue的删除过程就是两个过程:

    • 将需要删除位置的元素和最后子树的元素进行置换并且设置最右子树值为NULL
    • 通过siftDownComparable()方法将待删除位置的新元素进行下沉直至符合最小堆要求
    • 当然由于PriorityBlockingQueue是线程安全的,所以删除操作通过ReentrantLock的lock实现先锁后操作的流程。
    public E poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                return dequeue();
            } finally {
                lock.unlock();
            }
        }
    
        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;
        }
    
        public E poll(long timeout, TimeUnit unit) throws InterruptedException {
            long nanos = unit.toNanos(timeout);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            E result;
            try {
                while ( (result = dequeue()) == null && nanos > 0)
                    nanos = notEmpty.awaitNanos(nanos);
            } finally {
                lock.unlock();
            }
            return result;
        }
    
        public E peek() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                return (size == 0) ? null : (E) queue[0];
            } finally {
                lock.unlock();
            }
        }
    
    private void removeAt(int i) {
            Object[] array = queue;
            int n = size - 1;
            if (n == i) // removed last element
                array[i] = null;
            else {
                E moved = (E) array[n];
                array[n] = null;
                Comparator<? super E> cmp = comparator;
                if (cmp == null)
                    siftDownComparable(i, moved, array, n);
                else
                    siftDownUsingComparator(i, moved, array, n, cmp);
                if (array[i] == moved) {
                    if (cmp == null)
                        siftUpComparable(i, moved, array);
                    else
                        siftUpUsingComparator(i, moved, array, cmp);
                }
            }
            size = n;
        }
    

      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
                while (k < half) {
                    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;
            }
        }
    

    PriorityBlockingQueue的删除过程图解

    PriorityBlockingQueue的删除过程图解-1 PriorityBlockingQueue的删除过程图解-2 PriorityBlockingQueue的删除过程图解-3

      结合上面的图解,我们来说明一下二叉堆的出队过程:

      1. 将找出队尾的元素8,并将它在队尾位置上删除(图2);
      1. 此时队尾元素8比根元素1的最小孩子3要大,所以将元素1下移,交换1和3的位置(图3);
      1. 然后此时队尾元素8比元素1的最小孩子4要大,继续将1下移,交换1和4的位置(图4);
      1. 然后此时根元素8比元素1的最小孩子9要小,不需要下移,直接将根元素8赋值给此时元素1的位置,1被覆盖则相当于删除(图5),结束。

    参考文章

    给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

    相关文章

      网友评论

        本文标题:java源码-PriorityBlockingQueue

        本文链接:https://www.haomeiwen.com/subject/xkgvvftx.html