并发()——PriorityBlockingQueue源码解析

在这里插入图片描述

PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现。本身是线程安全的,内部使用显示锁ReentrantLock

一、PriorityBlockingQueue类图结构

在这里插入图片描述

PriorityBlockingQueue类中的成员变量与成员方法介绍:

  • queue数组:用来存放队列元素
  • size:用来存放队列元素的个数
  • allocationSpinLockOffset:用来在扩容队列时候做cas,目的是保证只有一个线程可以进行扩容。
  • comparator:由于这是一个优先级队列,所以有一个比较器comparator用来比较元素的大小。
  • lock:独占锁对象用来控制同时只能由一个线程可以进行入队出队操作
  • notEmpty条件变量:用来实现take方法的阻塞模式
  • 这里没有notFull条件变量是因为这里的put操作是非阻塞的,为什么要设计为非阻塞的是因为这是无界队列。
  • PriorityQueue:用来做序列化的。

二、使用该类的情况

一个典型的需求是:当你需要使用一个有序列表的数据结构的时候,java提供的PriorityBlockingQueue类就拥有这一种功能。

你想要添加到 PriorityBlockingQueue中的所有元素都必须实现Cmparable接口。这个接口有一个compareTo方法,它接收同样类型的对象,如有两个比较的对象:一个是执行这个方法的对象,另一个是作为参数接收的对象。如果本地对象小于参数,则该方法返回小于0的数值。如果本地对象大于参数,则该方法返回大于0的数值。如果本地对象等于参数,则该方法返回等于0的数值、

PriorityBlockingQueue使用 compareTo方法决定插入元素的位置。

三、构造函数

在这里插入图片描述

在这里插入图片描述

三、offer操作

在这里插入图片描述

下面来看看上图中红色框圈起来主要的两个方法

在这里插入图片描述

从上图可以看出,在扩容的时候,首先要进行释放锁操作,释放锁的目的是想要使用cas来控制只有一个线程能够扩容成功,而不是使用全局锁,因为全局锁的粒度更大,使用全局锁后其他线程的入队出队都不能进行,这样会降低并发性。

扩容的大小有两种:

  • 一种是如果旧的容量小于64,那么新的容量为:2 * 旧的容量 + 2,
  • 如果旧的我拿过来大于64,那么新的容量为 1.5 * 旧的容量

上图中【第287行】 是获取锁,为什么在该方法的最开始要释放锁,在该方法的后面又要获取锁呢?这里是因为可见性的问题,因为queue没有被volatile修饰。另外可能在扩容的时候进行了出队操作,如果直接进行拷贝的话,可能看到的数组元素不是最新的,而通过调用 lock 后,获取的数组则是最新的,并且在释放锁之前数组的内容不会发生变化,因为其他线程不能进行入队出队操作。

在这里插入图片描述

poll操作

在这里插入图片描述

在这里插入图片描述

extract方法的执行过程:

  • 【步骤一】首先获取队列的头元素,放到变量result
  • 【步骤二】然后获取队列尾部元素,放到变量x中,并将队尾的元素置为 null
  • 【步骤三】根据默认的比较器或者用户提供的比较器来调整小根堆

在这里插入图片描述

假如当前队列元素:

在这里插入图片描述

对应的树结构为:

在这里插入图片描述

这个时候调用poll方法,那么对应的extract方法中【步骤一】中的result = 2, x = 11;然后在【步骤二】中,将队尾的元素置为null。所以这个时候树的结构如下:

在这里插入图片描述
我们根据源码来分析各个数值:

在这里插入图片描述

然后可以看到result= 2 的 leftChildVal = 4,rightChildVal = 6,n = 5, half = 2;因为 4 < 6 , 所以 c = 4,也就是获取根节点的左右孩子值最小的的哪一个;然后看11 > 4 ,也就是 key > c 所以【第389行】不成立,直接执行array[k] = c。即把c放入树根,现在的树为:

在这里插入图片描述

然后看根的左孩子4为根的字树,我们要为这个子树找到根节点。
在这里插入图片描述
leftChildVal = 8, rightChildVal = 10, 8 < 10, 所以c = 8,也就是获取根节点的左孩子值最小的,然后看11 > 8 , 所以【第389行】不成立,直接执行array[k] = c。即把c放入树根,现在的树为:

在这里插入图片描述

这是 k = 3, half = 3,退出while循环,执行【第394行】,即代码 array[k] = key; 这个时候的树为:

在这里插入图片描述

对应的队列为:

在这里插入图片描述

size操作

在这里插入图片描述

因为size操作,添加了独占锁ReentrantLock,所以获取的队列中元素的个数是精确的。

总结

一、PriorityBlockingQueue类是一个优先级队列,本身是线程安全的,内部使用显示锁ReentrantLock来保证线程安全。PriorityBlockingQueue存储的对象必须是实现了 Comparable接口的,因为PriorityBlockingQueue队列会根据内存存储的每一个元素的comparaTo方法来比较每个元素的大小。这样在take的时候会根据优先级,将优先级最小的取出。

二、PriorityBlockingQueue类似于 ArrayBlockingQueue,其内部都使用了一个独占锁来控制同时只有一个线程可以进行入队和出队的操作,另外PriorityBlockingQueue只使用了 notEmpty 条件变量,而没有使用 notFull变量,这是因为前者是无界队列,当put的时候永远都不会处于await

三、PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素的个数 >= 最大容量的时候会通过算法进行扩容操作。这里的扩容是通过释放锁使用cas来实现的。

扩容的大小有两种:

  • 一种是如果旧的容量小于64,那么新的容量为:2 * 旧的容量 + 2,
  • 如果旧的我拿过来大于64,那么新的容量为 1.5 * 旧的容量

四、PriorityBlockingQueue类的offerpoll 方法在放入数据和取出数据的时候分别会进行小根堆的建立siftUpComparable,和小根堆的调整siftDownComparable

参考并感谢

[1] http://ifeve.com/concurrent-collections-4/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值