三十一、阻塞队列之PriorityBlockingQueue

63 阅读7分钟

阻塞队列之PriorityBlockingQueue

PriorityBlockingQueue基本概念

  • PriorityBlockingQueue不满足队列先进先出的原则
  • PriorityBlockingQueue基于二叉堆实现,底层使用数组结构实现二叉堆
  • PriorityBlockingQueue会将存入的数据进行排序

二叉堆介绍

二叉堆其实就是一个完整的二叉树。所谓完整的二叉树是指只有当一层节点已经添加满了,才会将元素添加到下一层

二叉堆分为小顶堆和大顶堆,PriorityBlockingQueue使用的是小顶堆

小顶堆是指任意子节点都比其父节点大。而大顶堆正好相反,任意子节点都比其父节点小

二叉堆是通过数组结构实现小顶堆的

image.png

PriorityBlockingQueue构造方法

PriorityBlockingQueue是无界队列。虽然其有个指定容量的构造方法,但是在添加元素的方法中,当容量不够时,还是会进行扩容,直到最大值MAX_ARRAY_SIZE。

public LinkedBlockingQueue() {
	this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
	if (capacity <= 0) throw new IllegalArgumentException();
	this.capacity = capacity;
	last = head = new Node<E>(null);
}

PriorityBlockingQueue核心属性

// 默认队列容量大小为11
private static final int DEFAULT_INITIAL_CAPACITY = 11;

// 数组的最大容量。为了适配不同的JVM,所以设置成Integer.MAX_VALUE - 8
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;

PriorityBlockingQueue存放元素方法

add方法

public boolean add(E e) {
	return offer(e);
}

offer非阻塞方法

  1. 判断数据是否为空,如果为空,抛出异常
  2. 加锁
  3. 判断数组中元素个数是否达到数组最大值,如果达到,数组扩容
  4. 如果数组容量小于等于64,扩容成原来容量的2倍+2
  5. 如果数组容量大于64,扩容成原来的1.5倍
  6. 判断扩容后数组的容量是否达到最大值,如果达到最大值
  7. 判断扩容前数组容量是否已达到最大值,如果达到,抛出异常
  8. 如果未达到,设置扩容后的数组容量为最大值
  9. 判断比较器是否为空,如果为空,使用默认比较器,如果不为空,使用指定比较器
  10. 数据放入数组中,并且排序成小顶堆结构
  11. 数组中元素个数size值+1
  12. 唤醒消费者
  13. 释放锁
public boolean offer(E e) {
	if (e == null)
		// 参数为空,抛异常
		throw new NullPointerException();
	final ReentrantLock lock = this.lock;
	lock.lock();
	// n是元素个数,cap是数组容量
	int n, cap;
	Object[] array;
	while ((n = size) >= (cap = (array = queue).length))
		// 元素个数>=数组容量,扩容
		tryGrow(array, cap);
	try {
		Comparator<? super E> cmp = comparator;
		// 将元素放入数组中,并且与数组中的元素比较大小,保持小顶堆结构
		if (cmp == null)
			// 初始化PriorityBlockingQueue时没有指定比较器,走这里
			siftUpComparable(n, e, array);
		else
			// 初始化PriorityBlockingQueue时指定了比较器,走这里
			siftUpUsingComparator(n, e, array, cmp);
		size = n + 1;
		// 唤醒消费者
		notEmpty.signal();
	} finally {
		lock.unlock();
	}
	return true;
}
// 队列扩容的方法
private void tryGrow(Object[] array, int oldCap) {
	// 扩容时是释放锁的
	lock.unlock();
	Object[] newArray = null;
	if (allocationSpinLock == 0 &&
		UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,0, 1)) {
		// 进入此处说明当前线程拿到给数组扩容的资格,并且已经将扩容标志属性设置为1
		try {
			// 如果数组旧容量<64,扩容成旧容量的2倍再+2
			// 这是为了在小容量的时候可以加速扩容
			// 如果数组旧容量>=64,扩容成旧容量的1.5倍,
			// 因为此时的旧容量已经比较大了,1.5倍的扩容速度比较好
			int newCap = oldCap + ((oldCap < 64) ?
						(oldCap + 2) : 
						(oldCap >> 1));
			if (newCap - MAX_ARRAY_SIZE > 0) { 
				// 进入此处说明新容量比数组最大容量还大
				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 {
			// 将扩容标志属性设置为0
			allocationSpinLock = 0;
		}
	}
	if (newArray == null) 
		// 当前线程进入此方法,但是没有给数组扩容的资格,想等待其他线程扩容
		Thread.yield();
	// 重新竞争锁
	lock.lock();
	if (newArray != null && queue == array) {
		// 进入此处说明没有发生扩容并发,将新数组赋值给queue
		queue = newArray;
		// 旧数组中的元素迁移到新数组中
		System.arraycopy(array, 0, newArray, 0, oldCap);
	}
}
// k:队列元素个数;x:要存放的元素;array:存元素的数组
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赋值为父节点索引
		// 准备在下次循环中,让当前元素与父节点的父节点再比较
		k = parent;
	}
	// 到这里,有两种情况
	// 第一种,数组中没有元素,当前元素放在第一位就可以了
	// 第二种,数组经过while循环,已经找到自己的位置
	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;
}

offer阻塞方法

// 因为PriorityBlockingQueue是无界队列,所以存放数据不会阻塞
public boolean offer(E e, long timeout, TimeUnit unit) {
	return offer(e); // never need to block
}

put方法

public void put(E e) {
	offer(e);
}

PriorityBlockingQueue获取元素方法

remove方法

public E remove() {
	E x = poll();
	if (x != null)
		return x;
	else
		throw new NoSuchElementException();
}

poll无参方法

public E poll() {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		// dequeue方法拿到数据,并且保持小顶堆结构
		return dequeue();
	} finally {
		lock.unlock();
	}
}

poll有参方法

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

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;
}
private E dequeue() {
	// n为最后一个元素的索引
	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;
	}
}
// k:默认值0;x:数组中最后一个元素;array:数组;n:最后一个元素索引
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; 
			// 左子结点元素
			Object c = array[child];
			// 右子节点索引
			int right = child + 1;
			// 判断右子节点是否存在
			// 并且判断左子结点元素是否大于右子节点
			if (right < n &&
				((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
				// 左子结点元素大于右子节点,右子节点赋值给c
				c = array[child = right];
			// 判断数组最后一个元素是否小于等于左右子节点中较小的那个节点
			if (key.compareTo((T) c) <= 0)
				// 最后一个元素小于等于左右子节点中较小的那个节点
				// 结束循环
				break;
			array[k] = c;
			k = child;
		}
		array[k] = key;
	}
}