NOTE

6.41 PriorityBlockingQueue

1. 是什么 底层使用数组(二叉堆)实现的无界的阻塞队列 读读、读写、写写相互阻塞 可以排序 由于无界,所以put操作不会阻塞,但是take操作会阻塞(队列为空的时候) 1.1. 二叉堆 一颗完全二叉树,堆序性质为,每个节点的值都小于其左右子节点的值,二叉堆中最小的值就是根节点。 底层用数组进行存储

Java创建于 更新于 historical

这是历史学习笔记,可能存在过时或不完整的理解。

1. 是什么

底层使用数组(二叉堆)实现的无界的阻塞队列 读读、读写、写写相互阻塞 可以排序 由于无界,所以put操作不会阻塞,但是take操作会阻塞(队列为空的时候)

1.1. 二叉堆

一颗完全二叉树,堆序性质为,每个节点的值都小于其左右子节点的值,二叉堆中最小的值就是根节点。 底层用数组进行存储。对于数组中的元素 a[i],其左子节点为 a[2i+1],其右子节点为 a[2i + 2],其父节点为 a[(i-1)/2]。 结构如下图:

2. 如何使用

public class PriorityBlockingQueueTest
{
    public static void main(String[] args) throws InterruptedException
    {
        PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(1);
        CountDownLatch latch = new CountDownLatch(2);

        new Thread(()->{
            for (int i = 0;;i++)
            {
                try
                {
                    String data = "data" + i;
                    queue.put(data);
                    System.out.println("Producer放入消息:" + data);
                    TimeUnit.SECONDS.sleep(1);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
                finally
                {
                    latch.countDown();
                }
            }
        }).start();

        new Thread(()->{
            for (;;)
            {
                try
                {
                    System.out.println("Consumer获取消息:" + queue.take());
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
                finally
                {
                    latch.countDown();
                }
            }
        }).start();

        latch.await();

    }
}

3. 原理分析

3.1. 构造方法

3.1.1. 底层使用数组+Lock+Condition实现

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {


	//底层使用数组实现(堆)
	private transient Object[] queue;
	//实际使用的长度
    private transient int size;

    //comparator确定元素的顺序,如果是null那么是自然序
    private transient Comparator<? super E> comparator;

    //只有一把锁说明读写互斥
    private final ReentrantLock lock;

	//只有一个condition说明只有读或者写的操作是阻塞的
    //当队列不为空的时候唤醒读操作
    private final Condition notEmpty;

	// 这个也是用于锁,用于数组扩容的时候,需要先获取到这个锁,才能进行扩容操作
	// 其使用 CAS 操作
	private transient volatile int allocationSpinLock;

	public PriorityBlockingQueue() {
		//默认11个,自然序
        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];
	}
}

3.2. put

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

3.2.1. 转调offer,不需要阻塞

  • offer
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;
		//自然序。把e加入到数组array末尾的位置n,然后与父亲比较,若是比父亲小则交换位置
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        //唤醒读者
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}
  • 6行:加锁。一旦该写者加了锁,那么其他读写线程不能进来操作
  • 10-11行:根据需要进行扩容
  • 13-18行:插入数组末尾,并且通过上浮操作保持堆的性质
  • 19行:队列中元素的实际数量+1
  • 21行:其他读者可能在队列为空的时候阻塞,这里需要唤醒 由上面的代码可以看出写的时候是不需要阻塞的,因为这个队列是无界的
3.2.1.1. 加锁
final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
} finally {
    lock.unlock();
}
3.2.1.2. 判断是否需要扩容
//如果当前队列中的元素个数 >= 数组的大小,那么需要扩容了
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
3.2.1.2.1. 需要的话进行扩容
  • tryGrow
private void tryGrow(Object[] array, int oldCap) {
	//为什么这里释放锁?让读的线程可以读而不至于再扩容的时候阻塞
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    //allocationSpinLock为0表示没有其他进行扩容,1表示有
    //当没有其他线程扩容 且 当前线程CAS加锁成功才进行扩容
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
        	//如果旧容量<64,那么新容量=2*旧容量+2
        	//否则为1.5*旧容量
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
           //溢出判断
            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;
            }
        	//确实有扩容 且 array没有变动--说明没有其他线程在扩容?
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
        	//释放锁
            allocationSpinLock = 0;
        }
    }
    //其他线程在扩容,让出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);
    }
}
3.2.1.3. 把元素加入堆的末尾
//自然序。把e加入到数组array末尾的位置n,然后与父亲比较,若是比父亲小则交换位置
if (cmp == null)
    siftUpComparable(n, e, array);
3.2.1.3.1. 上浮操作调整堆
  • siftUpComparable
//把x,插入到堆数组array,的k位置
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
	//最多调整到root即0
    while (k > 0) {
    	//父节点的位置 (k-1)/2
        int parent = (k - 1) >>> 1;
        Object e = array[parent];

        //如果x比父节点大,那么退出
        if (key.compareTo((T) e) >= 0)
            break;

        //否则与父节点交换位置
        array[k] = e;
        //从父节点继续往上
        k = parent;
    }
    //走到这里说明k位置存放x满足二叉堆的性质:比父节点大,比左右孩子小
    array[k] = key;
}
3.2.1.3.2. 调整的过程图

3.3. take

 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;
}
  • 4行:加锁。一旦该读者加了锁,那么其他读写线程不能进来操作
  • 8-9行:出队,如果队列为空那么进行阻塞,等待队列不为空的时候由写者唤醒

3.3.1. 加锁

 final ReentrantLock lock = this.lock;
    //加锁
    lock.lockInterruptibly();
    try {
    	//...
    } finally {
    	//解锁
        lock.unlock();
    }

3.3.2. 一直阻塞等待,直到出队成功

//出队元素为空那么阻塞等待唤醒
while ( (result = dequeue()) == null)
    notEmpty.await();
3.3.2.1. 出队具体操作
  • dequeue
private E dequeue() {
	//队列为空返回null
    int n = size - 1;
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        //root节点,即0号位置就是出队的元素
        E result = (E) array[0];

        E x = (E) array[n];//数组末尾的元素x
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
         	//把数组末尾的元素x放到0号位置,调整堆
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}
  • 7-12行:移除堆顶,末尾元素放到堆顶
  • 14-18行:下沉操作调整堆
3.3.2.1.1. 移除堆顶,末尾元素放到堆顶
Object[] array = queue;
//root节点,即0号位置就是出队的元素
E result = (E) array[0];

E x = (E) array[n];//数组末尾的元素x
array[n] = null;
3.3.2.1.2. 下沉操作调整堆
  • siftDownComparable
//把元素x,插入到长度为n,的堆数组array的,k位置
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;           
        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];
            //c是左右孩子中较小的那个
            //如果要插入的元素比左右孩子都小,那么二叉堆性质以满足,无需调整
            if (key.compareTo((T) c) <= 0)
                break;
            //否则将较小的孩子上移
            array[k] = c;
            //继续往下调整
            k = child;
        }
        //走到这里说明k位置存放x满足二叉堆的性质:比父节点大,比左右孩子小
        array[k] = key;
    }
}
3.3.2.1.3. 调整的过程图

4. 总结

无界队列,底层使用二叉堆实现,有序。 写不阻塞,读阻塞