并发集合类(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();
    }
}
相关推荐
吠品2 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
2601_953720822 小时前
【计算机毕业设计】基于Vue与Spring Boot的高校兼职信息服务平台设计与实现
spring boot·后端·课程设计
亚历克斯神3 小时前
智能搜索系统的升级复盘——从 Elasticsearch 到混合检索的检索质量提升
java·spring·微服务
再吃一根胡萝卜3 小时前
用 Rust 写一个桌面悬浮图标:为什么它比 Python 更适合 AI 桌面工具?
后端
IT_陈寒4 小时前
Vue的computed属性把我坑惨了,原来我一直用错姿势
前端·人工智能·后端
金銀銅鐵4 小时前
[Java] 一个方法最多可以有多少个入参?
java·jvm
哭哭啼4 小时前
JAVA服务问题诊断
java·开发语言·jvm
evans在进步4 小时前
Spring Boot 核心机制详解:可执行 JAR、CORS、静态资源与配置绑定
spring boot·后端·jar
Sayuanni%35 小时前
SpringBoot 从注解到源码:核心知识点总结
java·spring boot·后端
坚定信念,勇往无前5 小时前
Maven 私有仓库-nexus
java