java集合之PriorityBlockingQueue

PriorityBlockingQueue是一个阻塞队列,它是有界的,有最大容量,一般达不到,虽然它是队列,它并不能满足先进先出规则,它是按优先级出队,它会将插入的数据进行排序,按优先级出队,底层采用数组实现的二叉堆,所以当我们往队列中添加自定义对象时,自定义对象需要实现Comparable接口,也就是需要告诉队列,你的数据需要按什么规则进行排序。具体情况,我们根据源码来解释
在这里插入图片描述

1、重要属性
	// 默认容量
	private static final int DEFAULT_INITIAL_CAPACITY = 11;
    // 最大容量
    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;
	// 等待队列
    private final Condition notEmpty;
	// 因为PriorityBlockingQueue的底层是基于二叉堆的,而二叉堆又是基于数组实现的,
	// 数组长度是固定的,如果需要扩容,需要构建一个新数组。PriorityBlockingQueue在做扩容操作时,
	// 不会lock住的,释放lock锁,基于allocationSpinLock属性做标记,来避免出现并发扩容的问题。 
    private transient volatile int allocationSpinLock;
2、构造方法
	// 无参构造
	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.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }
    // 传入集合作为构造参数,表示要将集合中的数据放入队列
    public PriorityBlockingQueue(Collection<? extends E> c) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        boolean heapify = true; // true if not known to be in heap order
        boolean screen = true;  // true if must screen for nulls
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            heapify = false;
        }
        else if (c instanceof PriorityBlockingQueue<?>) {
            PriorityBlockingQueue<? extends E> pq =
                (PriorityBlockingQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            screen = false;
            if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                heapify = false;
        }
        Object[] a = c.toArray();
        int n = a.length;
        // If c.toArray incorrectly doesn't return Object[], copy it.
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, n, Object[].class);
        if (screen && (n == 1 || this.comparator != null)) {
            for (int i = 0; i < n; ++i)
                if (a[i] == null)
                    throw new NullPointerException();
        }
        this.queue = a;
        this.size = n;
        if (heapify)
            heapify();
    }
3、写入操作
3.1 add(E e)

add方法没啥好说的,最终掉的是offer

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

此方法往队列中添加元素,成功返回true,虽然是有最大容量限制,但是几乎是达不到最大容量的,相当于无界队列,所以生产者是用不着阻塞的,最多是等待别的线程扩容

public boolean offer(E e) {
	// 元素为空 抛异常
    if (e == null)
        throw new NullPointerException();
    // 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    // 在添加数据之前,会采用while循环的方式,来判断当前元素个数是否大于等于数组长度。
    // 如果满足,需要执行tryGrow方法,对数组进行扩容
	// 如果两个线程同时执行tryGrow,只会有一个线程在扩容,另一个线程可能多次走while循环,
	// 多次走tryGrow方法,但是依然需要等待前面的线程扩容完毕
    while ((n = size) >= (cap = (array = queue).length))
    	// 扩容方法
        tryGrow(array, cap);
    try {
    	// 获取比较器
        Comparator<? super E> cmp = comparator;
        // 比较数据大小,存储数据,并根据优先级排序规则进行移动,保证二叉树平衡
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        // 元素个数加1
        size = n + 1;
        // 唤醒等待的读线程
        notEmpty.signal();
    } finally {
    	// 释放锁
        lock.unlock();
    }
    return true;
}

扩容方法,扩容前先释放锁资源,由于底层是数组结构,扩容需要申请新数组,将老数组的数据复制到新数组,若不释放锁,导致在扩容过程中其他的线程无法进行读写,比较影响效率,所以先释放锁,通过CAS改变allocationSpinLock 的值来保证只有一个线程可以进行扩容

private void tryGrow(Object[] array, int oldCap) {
	// 先释放锁
    lock.unlock(); 
    // 声明新数组
    Object[] newArray = null;
    // 如果allocationSpinLock 为0,说明当前没有线程在进行扩容
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
        	// 计算新数组的容量
            int newCap = oldCap + ((oldCap < 64) ?
            						// 如果数组长度比较小,这里加快扩容长度速度。(还可以避免数组长度为0的时候,无效的扩容,正常是不会出现的,除非瞎玩,比如反射)
                                   (oldCap + 2) : // grow faster if small
                                   // 如果长度大于等于64了,每次扩容到1.5倍即可。
                                   (oldCap >> 1));
            // 如果新数组长度大于MAX_ARRAY_SIZE
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
            	// 声明minCap,长度为老数组 + 1
                int minCap = oldCap + 1;
                // 老数组+1变为负数,或者老数组长度已经大于MAX_ARRAY_SIZE了,无法扩容了。
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                // 如果没有超过限制,直接设置为最大长度即可
                newCap = MAX_ARRAY_SIZE;
            }
            // 新数组长度,得大于老数组长度
            // queue == array 此处的判断是为了避免并发扩容,比如有一个线程刚刚扩容完毕,
            // 把newArray赋值给了queue,此时在外面进行while循环某个线程进来了,如果不判断
            // queue == array是否相等,就又申请了一个新数组,相当于进行了两次扩容
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
        	// 还原标记
            allocationSpinLock = 0;
        }
    }
    // 如果到了这,newArray依然为null,说明这个线程没有进到if方法中,去构建新数组
    if (newArray == null) // back off if another thread is allocating
    	// 稍微等一手,让扩容的线程把扩容完成
        Thread.yield();
    // 加锁
    lock.lock();
    // 确认是当前线程申请的新数组,且没有并发问题,则将新数组赋值给queue,并将老数组的内容拷贝到新数组
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

添加进来的数据,需要保证二叉堆的平衡,也就是要保证按优先级出队,所以添加进来的数据可能是需要调整的,寻找正确的位置再放下

// k当前数组元素的个数,也就是x要放的位置,那就是说默认想把它放在队尾
// x是需要加入队列的数据
// array是数组
private static <T> void siftUpComparable(int k, T x, Object[] array) {
	// 将插入的元素直接强转为Comparable 所以自定义数据类型,必须实现Comparable 
    // 这行强转,会导致添加没有实现Comparable的元素,直接报错。
   Comparable<? super T> key = (Comparable<? super T>) x;
   // k>0,说明队列中有数据,有数据才需要调整位置,没有数据,默认放在0位置就可以
    while (k > 0) {
    	// 找到父节点 (k - 1) >>> 1 找父节点的公式
        int parent = (k - 1) >>> 1;
        // 拿到父节点的值
        Object e = array[parent];
        // 如果当前加入队列的元素比父节点大,则直接放
        if (key.compareTo((T) e) >= 0)
        	// 跳出循环
            break;
        // 如果加入的元素比父节点小,则将父节点放到k位置,也就是父节点往下移动
        array[k] = e;
        // 重新设置x节点需要放置的位置。k就来到了父节点原本的位置,此时再进行while判断,
        // 如果合适则放下,不合适继续重复刚才的操作,直到找到合适的位置,或者找到根节点
        k = parent;
    }
    array[k] = key;
}

在这里插入图片描述

3.3 put(E e)

参考offer方法

public void put(E e) {
    offer(e); // never need to block
}
3.4 offer(E e, long timeout, TimeUnit unit)

参考offer方法

public boolean offer(E e, long timeout, TimeUnit unit) {
    return offer(e); // never need to block
}
4、读取操作

读取操作是存在线程挂起的情况的,因为如果数组中元素个数为0,当前线程如果执行了take方法,必然需要挂起。
其次获取数据,因为是优先级队列,所以需要从二叉堆栈顶拿数据,直接拿索引为0的数据即可,但是拿完之后,需要保持二叉堆结构。

4.1 poll()

出队,从队列头部取出元素之后,那么根节点就没有了,此时需要调整二叉堆,找出新的跟节点,保证二叉堆的结构平衡,且按优先级出队

public E poll() {
	// 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 执行出队
        return dequeue();
    } finally {
    	// 释放锁
        lock.unlock();
    }
}
private E dequeue() {
	// n是尾巴元素的下标
    int n = size - 1;
    // n小于0,说明队列中没有元素
    if (n < 0)
        return null;
    else {
    	// 获取queue
        Object[] array = queue;
        // 拿到0位置的元素,就是要返回的元素
        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);
        // 元素个数减1
        size = n;
        // 返回result
        return result;
    }
}
// k是0,默认是下标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;           // loop while a non-leaf
        // 做了超过一半,就不需要再往下找了。
        while (k < half) {
        	// 找左子节点索引,一个公式,可以找到当前节点的左子节点
            int child = (k << 1) + 1; // assume left child is least
            // 左子节点的值
            Object c = array[child];
            // 右子节点的下标
            int right = child + 1;
            // right < n 判断是否有右子节点
            // 判断左节点是否大于右节点
            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重置到之前的子节点位置,重新循环,重新给末尾元素寻找合适的位置
            k = child;
        }
        // 上面while循环搞定后,可以确认整个二叉堆中,数据已经移动ok了,只差当前k的位置数据是null
        // 将最后一个索引的数据放到k的位置
        array[k] = key;
    }
}
4.2 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;
}
4.3 poll(long timeout, TimeUnit unit) throws InterruptedException
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;
}
### Java 集合框架中的并发特性和线程安全实现 #### 并发集合概述 Java集合框架提供了一系列工具来处理不同类型的集合,其中包括支持并发操作的类。这些类不仅能够提高程序效率,还简化了开发人员编写多线程应用程序的工作。 #### PriorityBlockingQueue 特性 `PriorityBlockingQueue` 是一种特殊的阻塞队列,在Java 1.5版本中被引入至 `java.util.concurrent` 包内[^1]。此队列按照优先级顺序排列元素,并允许生产者和消费者模式下的线程间通信。其核心特点在于利用优先级堆结构保障有序存取的同时实现了线程安全性。 #### 线程安全保障机制 为了确保多个线程可以安全地访问共享资源而不发生冲突,Java并发库采用了多种策略和技术: - **内置锁机制**:通过ReentrantLock等可重入锁控制对临界区代码段的独占使用权; - **条件变量**:借助Condition接口管理等待/通知逻辑,使得某些条件下才能执行的任务得以有效协调; - **原子更新器**:对于简单数值型字段的支持,可以直接使用AtomicInteger, AtomicLong等类完成无竞争状态改变; 例如,在`PriorityBlockingQueue.put()` 方法里就运用到了上述提到的第一项技术——当尝试向已满队列入新条目时会先获取锁再判断是否需要唤醒其他处于休眠态准备消费数据的线程[^3]。 ```java public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); // 使用条件变量notFull进行等待 enqueue(e); // 插入元素e到队列中 } finally { lock.unlock(); // 解除锁定以便让出CPU给别的线程运行 } } ``` 此外,还有像ConcurrentHashMap这样的哈希表映射容器也体现了良好的设计思路,它允许多个读写操作几乎完全并行化从而极大提升了吞吐量[^2]。 #### 实际应用案例分析 考虑一个简单的消息传递场景,发送方不断往队列里面放入信息,接收端则负责从中提取出来做进一步处理。这里选用`LinkedBlockingQueue`作为中介载体,因为它的FIFO性质非常适合此类需求[^4]。 ```java // 创建固定容量大小的消息缓冲池 private static LinkedBlockingQueue<String> messageBuffer = new LinkedBlockingQueue<>(10); /** * 向队列追加一条记录 */ public static boolean addToQueue(String msg){ return messageBuffer.offer(msg); } /** * 从队列移除最先进来的那条记录 */ public static String takeFromQueue(){ try{ return messageBuffer.take(); // 若当前为空,则一直等到有东西进来为止 }catch(InterruptedException ex){ Thread.currentThread().interrupt(); throw new RuntimeException(ex.getMessage(),ex); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值