并发集合类(4):PriorityBlockingQueue

什么是PriorityBlockingQueue?

PriorityBlockingQueueJUC中一个线程安全的无界阻塞 优先级队列,它结合了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

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

  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();
    }
}
相关推荐
环境栈笔记10 分钟前
指纹浏览器安全评测方法:核对环境、数据与权限后再选型
前端·人工智能·后端·自动化
星核0penstarry13 分钟前
DeepSeek-V4-Flash 正式公测:大模型行业进入「极速平价普惠时代」
java·开发语言·人工智能
卷无止境36 分钟前
LangExtract:让LLM从杂乱文本中"抠"出结构化数据的开源工具
后端·python
xieliyu.42 分钟前
Java 线程池入门详解:线程池基础定义 + ThreadPoolExecutor 全参构造方法逐参数解析
java·开发语言·idea·javaee
卷无止境1 小时前
软件复杂度:那些让代码变得难以理解的东西,到底能不能量化?
后端·python
老白干3 小时前
jjwt 0.9.1 在 JDK 11+ 上的两个“坑”与完整解决方案
java·python·log4j
圆山猫9 小时前
[Virtualization](四):Linux KVM/RISC-V 的 vCPU 运行路径
java·linux·risc-v
城管不管10 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
IT小白杨10 小时前
从环境制备到自动化工作流:多账号运营的工程化架构拆解
java·经验分享·自动化·安全架构·指纹浏览器
豆瓣鸡10 小时前
算法日记 - Day3
java·开发语言·算法