并发容器(三) — PriorityBlockingQueue(优先队列)源码解析

一、概述

  1. PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。
  2. 队列内部使用 数组 存储数据。
  3. 由于数组初始化时,长度已经确定,而PriorityBlockingQueue又是一个无界队列,因此内部存在自动扩容机制 (类似ArrayList)。
  4. PriorityBlockingQueue 使用最大堆来实现。

本文涉及的知识点:

  1. 数组实现的相关数据结构:ArrayList 源码分析
  2. 队列的相关知识:Queue 综述
  3. 阻塞队列的概念:并发容器(一) — 综述
  4. ArrayBlockingQueue源码解析
  5. 阻塞功能的实现涉及Condition接口:Condition接口
  6. 并发安全涉及可重入锁ReentrientLock:ReentrantLock源码解析
  7. 二叉堆 - 最小堆

二、源码解析

普通队列包含入队和出队两种操作,阻塞队列在普通队列原有的基础上实现了阻塞功能。

阻塞队列提供了4套API 来操作队列,本文我们只分析阻塞相关的操作 (入队/出队)。其他操作可以参考:ArrayBlockingQueue源码解析

下面将从以下几个方面进行分析:

  1. 构造函数: PriorityBlockingQueue()
  2. 最小堆的上浮、下沉操作:siftUpUsingComparator()siftDownUsingComparator()
  3. 队列具有阻塞功能的入队、出队操作:put(e)take()
  4. 队列的扩容操作:tryGrow()

1. 构造函数

PriorityBlockingQueue 中只维护了一个Condition,ArrayBlockingQueue 中维护了两个了Condition。

// PriorityBlockingQueue.class
final ReentrantLock lock; //可重入锁,在添加、移除元素时加锁保证安全性
private final Condition notEmpty; //锁的非空条件
private transient Comparator<? super E> comparator; //队列中元素跟新插入元素的比较器
final Object[] queue; //实际添加元素的容器
private transient int size; //队列中元素的个数

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

2. 最小堆的上浮、下沉操作

最小堆的上浮、下沉操作:siftUpUsingComparator()siftDownUsingComparator()

siftUpUsingComparator()

/* 
 * 最小堆:上浮操作
 * @param k 新添加的元素将要被插入的位置
 * @param x 新添加的元素
 * @param 存放数据的数组容器
 */
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; //将插入的元素添加到最小堆的合理位置。
}

/**
 * 最小堆:下沉操作
 * @param k the position to fill
 * @param x the item to insert
 * @param array the heap array
 * @param n heap size 堆大小
 */
private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
                                      int n, Comparator<? super T> cmp) {
    if (n > 0) {
        int half = n >>> 1; 
        while (k < half) {
            int child = (k << 1) + 1; //通过父节点查找左子节点
            Object c = array[child];
            int right = child + 1; //右侧子节点
            // 比较左右子节点大小,取较小子节点
            if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                c = array[child = right];
            // 取左右子节点较小值跟插入的元素比较,插入的元素小于子节点,则退出下沉操作。
            if (cmp.compare(x, (T) c) <= 0)
                break;
           	// 否则,交换子节点跟当前节点的位置
            array[k] = c; 
            k = child;
        }
        array[k] = x;
    }
}

3. 具有阻塞功能的入队、出队操作

队列具有阻塞功能的入队、出队操作:put(e)take()

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; //n表示队列中插入的元素个数,cap表示当前数组容器的大小。
    Object[] array;
    // 当前个数>=数组容器长度时,需要进行扩容操作,这个在第4部分分析
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap); //数组扩容
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
        	// 请参考第2部分,新插入的数据进行上浮操作
            siftUpComparable(n, e, array);
        else
        	// 请参考第2部分,新插入的数据进行上浮操作
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1; //队列元素个数增加1
        notEmpty.signal(); //通知获取数据时被阻塞的线程可以获取数据了。
    } finally {
        lock.unlock(); //释放锁
    }
    return true;
}

// *********************

// 出队列
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); //加锁,可响应中断
    E result;
    try {
        while ( (result = dequeue()) == null) //dequeue()==null表示当前队列为空
            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]; //将最小堆中最后一个元素插入到堆顶
        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;
    }
}

4. 队列的扩容操作

队列的扩容操作:tryGrow()

private void tryGrow(Object[] array, int oldCap) {
	// 释放锁(当前线程释放锁后,其他线程可以获取元素,使容量下降,其他插入元素的线程也可以不扩容直接向数组中添加元素,提高效率。)
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    // 自旋锁,CAS操作
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1)); //数组容量超过64个时,每次扩容50%
            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 == null 表示有线程正在扩容,所以当前线程要让出自己的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);
    }
}

三、小结

  1. PriorityBlockingQueue 是一个数组实现的具有优先级的无界阻塞列。
  2. 只有 put(e)take() 才能实现阻塞功能,其他方法都不具备阻塞功能。
  3. PriorityBlockingQueue 是通过最小堆来实现优先级的,队列头数据是最小的。
发布了158 篇原创文章 · 获赞 26 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Love667767/article/details/104962113