JAVA并发包(十三):PriorityBlockingQueue

北城余情 提交于 2020-01-17 01:43:57

PriorityBlockingQueue中文名可以叫做优先阻塞队列,它的内部是数组结构的最小堆,能够保证每次取出的都是队列中最小的节点(比较器返回的最小元素)。但是它在数组中不是有序的b,只保证数组的第一个节点是最小的,也就是说第一个优先权最高,首先会被取出。

下图就是一个最小堆的示意图,它可以说是一个二叉树的结构,只需要保证父节点大于子节点,就满足最小堆的要求。然后就是从父节点开始,从上往下,从左往右把节点放到数组里。
在这里插入图片描述

参考:图解最小堆形成-以数组方式表示

一、内部代码结构

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

    /** 扩容锁,如果为1代表数组正在扩容 */
    private transient volatile int allocationSpinLock;

	

二、入队列

从下面代码看到,入队列方法最后都会调用offer()方法,通过重入锁做并发的控制。队列最大容量为Integer.MAX_VALUE - 8,调用阻塞方法一般不会受到容量的限制而阻塞,但是获取锁或者扩容的时候可能会等待。入队列会保持最小堆的数据结构。

	public boolean add(E e) {
        // 调用offer方法
        return offer(e);
    }

	public void put(E e) {
        offer(e); // never need to block
    }

	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;
    }

	/** 队列扩容方法 */
	private void tryGrow(Object[] array, int oldCap) {
		// 这里先释放锁,后面再次获取。因为扩容比较耗时,加锁会影响其他线程对队列的操作,比如操作出队列
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        // CAS操作allocationSpinLockOffset,也可以作为是一种加锁的方式,其他线程就不能扩容了
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
            	// 扩容两倍+2,或者1.5倍
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                // 这里看到队列是有最大容量限制的,最大为Integer.MAX_VALUE - 8
                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为空说明可能其他线程正在扩容,则避让cpu时间片,
        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);
        }
    }

	/** 没有初始化比较的入堆方法 */
	private static <T> void siftUpComparable(int k, T x, Object[] array) {
		// 对象必须是Comparable的实现类
        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;
    }

	/** 有比较器的入堆方法,逻辑跟上面方法一样,只是使用了比较器做比较 */
    private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator<? super T> cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = x;
    }

三、出队列

	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;
    }

	/** peek也会获取锁,然后返回数组的第一个节点 */
	public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (size == 0) ? null : (E) queue[0];
        } finally {
            lock.unlock();
        }
    }

	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;
        }
    }

	/** 取出第一个节点后,后面的节点往上提 */
	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;
        }
    }

四、遍历

如下遍历方法可以看到它只是把原来的数组复制了返回到迭代器对象,所以它是不会保证有序的。

 	public Iterator<E> iterator() {
        return new Itr(toArray());
    }

	public Object[] toArray() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return Arrays.copyOf(queue, size);
        } finally {
            lock.unlock();
        }
    }

五、删除

这里讲解删除的代码,我们就能比较了解上面出入队列的原理。假设如下图,我们要把26这个节点删除,根据上面出队列原理,我们会把14这个节点放到26的位置,然后再根据入队列的原理把14放到16的位置上。
在这里插入图片描述

	public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lock();
        try {
            int i = indexOf(o);
            if (i == -1)
                return false;
            removeAt(i);
            return true;
        } 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;
    }

六、总结

PriorityBlockingQueue在业务开发中比较少直接使用,理解它的原理对于后面学习定时线程池有很大的帮助。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!