并发集合类(4):PriorityBlockingQueue

什么是PriorityBlockingQueue?

PriorityBlockingQueue是JUC中一个线程安全的无界阻塞 优先级队列,它结合了PriorityQyeye以及BlockingQueue的特性。

与LinkedBlockingQueue以及ArrayBlockingQueue不同,PriorityBlockingQueue中的元素按照优先级出队,而不是FIFO。

因此PriorityBlockingQueue中的元素必须实现Comparable<T>,或者在构造时传入Comparator<T>

PriorityBlockingQueue的内部数据结构

java 复制代码
private static final int DEFAULT_INITIAL_CAPACITY = 11;

private transient Object[] queue;

private transient int size;

private transient Comparator<? super E> comparator;

private final ReentrantLock lock = new ReentrantLock();

@SuppressWarnings("serial") // Classes implementing Condition may be serializable.
private final Condition notEmpty = lock.newCondition();

private transient volatile int allocationSpinLock;

private PriorityQueue<E> q;

queue是真正的存储结构,代表一个平衡二叉堆。size代表它的大小。DEFAULT_INITIAL_CAPACITY是默认情况下,queue的大小。

comparator是排序规则,如果为null,代表使用元素自身的自然排序。

ReentrantLock是用于内部并发控制的唯一一把锁。notEmpty是空队列等待条件。

allocationSpinLock是扩容自旋锁,状态为1(扩容状态),或者0(没有进行扩容状态)。因此扩容不是在持有锁的情况下进行的。

q是为了兼容老版本而做出的字段。

PriorityBlockingQueue的方法

构造方法

java 复制代码
public PriorityBlockingQueue() {
    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.comparator = comparator;
    this.queue = new Object[Math.max(1, initialCapacity)];
}

在无参构造中,使用默认大小构造初始的队列大小,并且使用元素的自然排序。

其余构造方法可以指定队列的大小以及元素的排序规则。

offer操作

offer方法向队列中插入一个元素,且是不阻塞方法,又因为PriorityBlockingQueue是无界队列,因此必定返回true。

java 复制代码
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);
        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;
    if (allocationSpinLock == 0 &&
        ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
        try {
            int growth = (oldCap < 64)
                ? (oldCap + 2) // grow faster if small
                : (oldCap >> 1);
            int newCap = ArraysSupport.newLength(oldCap, 1, growth);
            if (queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    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. 加锁

  2. 循环判断此时判断当前size大于等于queue的容量。则调用tryGrow

    • 在扩容的时候,先释放主锁
    • 判断自旋锁是否被持有并尝试获取自旋锁,保证只有一个线程在扩容
    • 如果newArray为null,说明有其他线程在扩容,将CPU释放一次
    • 重新获取锁
    • 如果新数组还没被赋值,则完成赋值动作。
  3. 如果comparator为null,那么就使用元素自然排序,否则使用自定义的排序规则

  4. 容量加一,唤醒take线程

  5. 解锁

PriorityBlockingQueue的扩容规则如下:

  1. 当容量小于64,使用慢速扩容,每次只加2
  2. 当容量大于等于64,每次扩大为2倍

在扩容的过程中释放锁,增大了并发性。

put操作

因为是无界队列,因此put操作不需要被阻塞,所以put操作和offer操作完全一致。

java 复制代码
public void put(E e) {
    offer(e); // never need to block
}

poll操作

poll操作的作用是获取队列内部堆树的根节点元素,如果队列为空,则返回null。

java 复制代码
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return dequeue();
    } finally {
        lock.unlock();
    }
}
  1. 加锁
  2. 出队
  3. 释放锁

take操作

take操作的作用是获取队列内部堆树的根节点元素,如果队列为空则阻塞。

java 复制代码
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;
}
  1. 加锁
  2. 循环尝试出队,出队为空则阻塞
  3. 解锁

size方法

PriorityBlockingQueue使用size方法获取队列大小也是会加锁的,因此它也是准确值。

java 复制代码
public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return size;
    } finally {
        lock.unlock();
    }
}
相关推荐
Json____1 分钟前
用 FastAPI + Vue3 打造一套健身私教课预约管理系统
后端·fastapi·it学习·wwwoop.com
一条泥憨鱼17 分钟前
【Redis | 黑马点评】基于Redis的消息队列 | stream消息队列 源码解读
java·数据库·redis·缓存·lua
aibo86030820 分钟前
基于 Spring Boot + Vue3 的博物馆藏品管理系统设计与实现
java·spring boot·后端
苍何23 分钟前
Codex+Obsidian,多平台同步,一键分发,太香了!
后端
fanruitian24 分钟前
python 进程 线程
java·前端·python
\光辉岁月/28 分钟前
javaSE8-新特性-Stream&Option
java·stream·option
zzu123zsw31 分钟前
把条件校验收敛到枚举接口:Spring Boot 统一断言模式与零污染异常切面工程化
java·spring boot·后端·架构设计
SL_staff34 分钟前
计划系统落地难?从开发者视角重构执行入口体验
java·开源·全栈
鬼手点金1 小时前
单轮对话:最简单的 API 调用
java·服务器·前端·javascript·react.js·servlet·tomcat
牛牛牌蜡笔小新1 小时前
Java-常用类库
java·开发语言