并发集合类(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();
    }
}
相关推荐
NE_STOP1 小时前
Vide Coding--AI编程工具的选择
java
袋鱼不重1 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户8356290780511 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还1 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
Hommy881 小时前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api
码云数智-园园1 小时前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆1 小时前
JDK 下载安装与配置详细教程
java·前端·ai编程
CaffeinePro2 小时前
FastAPI响应处理:返回值、状态码、响应头与异常标准化与案例解析
后端
小宇宙Zz2 小时前
Maven依赖冲突
java·服务器·maven
swordbob2 小时前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio