PriorityBlockingQueue源码解析

1,714 阅读4分钟

PriorityBlockingQueue简介

PriorityBlockingQueue是一个具有优先级的阻塞队列。类似的阻塞队列用于实现生产消费模型比较方便,最常用的API有offerpolltake

  • offer:插入数据到队列中,不会阻塞,返回boolean表示成功还是失败
  • poll: 弹出队列中的第一个元素,非阻塞
  • poll(timeout):如果队列为空会阻塞最长timeout时间
  • take:一直阻塞,直到有数据就弹出数据。

PriorityBlockingQueue组成

   //默认容量11
   private static final int DEFAULT_INITIAL_CAPACITY = 11;
   /**
     * Priority queue represented as a balanced binary heap: the two
     * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
     * priority queue is ordered by comparator, or by the elements'
     * natural ordering, if comparator is null: For each node n in the
     * heap and each descendant d of n, n <= d.  The element with the
     * lowest value is in queue[0], assuming the queue is nonempty.
     */
    private transient Object[] queue;
    //数据个数
    private transient int size;
    //比较器
    private transient Comparator<? super E> comparator;
    //锁
    private final ReentrantLock lock = new ReentrantLock();
    //信号量
    private final Condition notEmpty = lock.newCondition();
    /**
     * Spinlock for allocation, acquired via CAS.
     */
    private transient volatile int allocationSpinLock;
    //用于序列化和反序列,一般时候为null
    private PriorityQueue<E> q;

PriorityBlockingQueue使用的是最小堆,也就是堆顶是最小值,一句comparator来进行排序,默认使用自然排序。可以看到queue是transient修饰的,无法序列化,序列化的时候会将queue中的数据拷贝到q中,为了适配老版本的PriorityBlockingQueue

offer

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    //加锁,线程安全
    lock.lock();
    int n, cap;
    Object[] es;
    //元素个数已经达到了队列容量,进行扩容
    while ((n = size) >= (cap = (es = queue).length))
        tryGrow(es, cap);
    try {
        final Comparator<? super E> cmp;
        if ((cmp = comparator) == null)
            siftUpComparable(n, e, es);//默认的排序方式
        else
            siftUpUsingComparator(n, e, es, cmp);//根据设置的cmp自定义排序方式
        size = n + 1;
        //通知不为空
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

扩容 tryGrow

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // 先释放锁
    Object[] newArray = null;
    //cas成功就进行扩容
    if (allocationSpinLock == 0 &&
        ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
        try {
            //原始容量小于64时扩容为2*oldCap + 2,大于等于64时等于1.5*oldCap,最大值为Integer.MAX_VALUE
            int growth = oldCap < 64 ? oldCap + 2 : oldCap >> 1;
            int newCap = ArraysSupport.newLength(oldCap, 1, growth);
            if (queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null) //cas失败,说明有其它线程cas成功,则自己尽量让出cpu
        Thread.yield();
    lock.lock();//加锁
    if (newArray != null && queue == array) {
        //如果是cas成功的线程获将会走到这里,结束扩容
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

tryGrow进来就先解锁是因为扩容是比较耗时的操作,所以使用cas提高性能,如果加锁就回到导致这个时候无法传入新数据。如果锁被cas失败的线程获取,就会自旋直到cas成功的线程获取到锁。因为tryGrow是在offer方法的循环中,会再次判断容量是否足够。

插入数据到最小堆 siftUpComparable

自定义的Comparator插入也同理

private static <T> void siftUpComparable(int k, T x, Object[] es) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    //k初始是插入元素的个数,假设k = 3,元素数组形式[a,b,c]
    //二叉堆形式:
    //  a
    //b    c
    while (k > 0) {
        //找到最后一个元素的父节点,相当于通过c的坐标找到a,即(2-1) >>> 1 = 0
        int parent = (k - 1) >>> 1;
        Object e = es[parent];
        if (key.compareTo((T) e) >= 0)//比父节点大,也就是父节点的右孩子,直接插入到下标为k的位置
            break;
        //和父节点交换位置,继续和当前的父节点比较,直到比父节点大或者已经是root节点了
        es[k] = e;
        k = parent;
    }
    es[k] = key;
}

经典的最小堆算法。

poll(timeout)

直接看poll(timeout)方法,因为这个方法其实包含了polltake方法的原理

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)//poll方法没有循环,获取不到数据直接返回null
            nanos = notEmpty.awaitNanos(nanos);//阻塞直到超时nanos或者其它线程调用了notEmpty.signal(),take中没有超时,直接使用notEmpty.await()
    } finally {
        lock.unlock();
    }
    return result;
}

小顶堆出队dequeue

private E dequeue() {
    // assert lock.isHeldByCurrentThread();
    final Object[] es;
    final E result;
    //头结点出队,然后调整堆,保持小顶堆
    if ((result = (E) ((es = queue)[0])) != null) {
        final int n;
        final E x = (E) es[(n = --size)];//堆尾元素
        es[n] = null;
        if (n > 0) {
            final Comparator<? super E> cmp;
            if ((cmp = comparator) == null)
                siftDownComparable(0, x, es, n);
            else
                siftDownUsingComparator(0, x, es, n, cmp);
        }
    }
    return result;
}

private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
        // assert n > 0;
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           // 自上而下,直到没有子节点
        while (k < half) {
            int child = (k << 1) + 1; // 从左孩子开始
            Object c = es[child];
            int right = child + 1;//右孩子
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
                c = es[child = right];//让c为比较小的那个节点
            if (key.compareTo((T) c) <= 0) //最后一个节点和比较小的那个节点比较,小于等于小的那个节点就退出循环,并将最后一个节点赋值给当前比较的子节点的父节点
                break;
            //最后一个节点比较大,将两个孩子节点中比较小的节点赋值到父节点
            es[k] = c;
            //继续往比较小的子树下一层找,然后继续和最后一个节点进行比较,可以想象成当前节点来到了比较小的节点的位置,继续往后比较
            k = child;
        }
        es[k] = key;
  }

小顶堆的移除看注释不太好弄明白,下面画个图举个例子

自上而下的和尾节点进行比较,每次选择较小的那个节点所在的子树继续比较,每次将最小的那个节点赋值给父节点。虽然最后一个节点还保留这,但是由于size已经减1了,所以引用不到。

一个特殊的例子

在这个例子首先会自定义一个Comparable,并且这个Comparable会认为所有的对象都是相同的。

特殊的Comparable类:

	private static class TestComparable implements Comparable<TestComparable> {
		int value = 0;
		public TestComparable(int value) {
			this.value = value;
		}
		@Override
		public int compareTo(TestComparable o) {
			return 0;
		}
		
	}

主程序:

	public static void main(String[] args) {
		PriorityBlockingQueue<TestComparable> queue = new PriorityBlockingQueue<>(10);
		for (int i = 0; i < 10; i++) {
			queue.add(new TestComparable(i));
		}
        System.out.print("[");
		for (int i = 0; i < 10; i++) {
			System.out.print(queue.poll().value);
        }
        System.out.println("]");
	}

以上程序最终的打印结果是 [0987654321] ,可以看到第一个打印的是第一个插入的节点,之后打印的顺序是和插入顺序反过来的,在认真回想一下小顶堆出队dequeue,就能更好的理解PriorityBlocking的出队逻辑。