问题

(1)PriorityBlockingQueue的实现方式?

(2)PriorityBlockingQueue是否需要扩容?

(3)PriorityBlockingQueue是怎么控制并发安全的?

简介

PriorityBlockingQueue是java并发包下的优先级阻塞队列,它是线程安全的,如果让你来实现你会怎么实现它呢?

还记得我们前面介绍过的PriorityQueue吗?点击链接直达【死磕 java集合之PriorityQueue源码分析

还记得优先级队列一般使用什么来实现吗?点击链接直达【拜托,面试别再问我堆(排序)了!

源码分析

主要属性

// 默认容量为11
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;
// 扩容的时候使用的控制变量,CAS更新这个值,谁更新成功了谁扩容,其它线程让出CPU
private transient volatile int allocationSpinLock;
// 不阻塞的优先级队列,非存储元素的地方,仅用于序列化/反序列化时
private PriorityQueue<E> q;

(1)依然是使用一个数组来使用元素;

(2)使用一个锁加一个notEmpty条件来保证并发安全;

(3)使用一个变量的CAS操作来控制扩容;

为啥没有notFull条件呢?

主要构造方法

// 默认容量为11
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];
}

入队

每个阻塞队列都有四个方法,我们这里只分析一个offer(E e)方法:


public boolean offer(E e) {
// 元素不能为空
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
int n, cap;
Object[] array;
// 判断是否需要扩容,即元素个数达到了数组容量
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条件
notEmpty.signal();
} finally {
// 解锁
lock.unlock();
}
return true;
} private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
// 取父节点
int parent = (k - 1) >>> 1;
// 父节点的元素值
Object e = array[parent];
// 如果key大于父节点,堆化结束
if (key.compareTo((T) e) >= 0)
break;
// 否则,交换二者的位置,继续下一轮比较
array[k] = e;
k = parent;
}
// 找到了应该放的位置,放入元素
array[k] = key;
}

入队的整个操作跟PriorityQueue几乎一致:

(1)加锁;

(2)判断是否需要扩容;

(3)添加元素并做自下而上的堆化;

(4)元素个数加1并唤醒notEmpty条件,唤醒取元素的线程;

(5)解锁;

扩容

private void tryGrow(Object[] array, int oldCap) {
// 先释放锁,因为是从offer()方法的锁内部过来的
// 这里先释放锁,使用allocationSpinLock变量控制扩容的过程
// 防止阻塞的线程过多
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// CAS更新allocationSpinLock变量为1的线程获得扩容资格
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// 旧容量小于64则翻倍,旧容量大于64则增加一半
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
// 判断新容量是否溢出
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
// 创建新数组
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
// 相当于解锁
allocationSpinLock = 0;
}
}
// 只有进入了上面条件的才会满足这个条件
// 意思是让其它线程让出CPU
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)解锁,解除offer()方法中加的锁;

(2)使用allocationSpinLock变量的CAS操作来控制扩容的过程;

(3)旧容量小于64则翻倍,旧容量大于64则增加一半;

(4)创建新数组;

(5)修改allocationSpinLock为0,相当于解锁;

(6)其它线程在扩容的过程中要让出CPU;

(7)再次加锁;

(8)新数组创建成功,把旧数组元素拷贝过来,并返回到offer()方法中继续添加元素操作;

出队

阻塞队列的出队方法也有四个,我们这里只分析一个take()方法:

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
E result;
try {
// 队列没有元素,就阻塞在notEmpty条件上
// 出队成功,就跳出这个循环
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
// 解锁
lock.unlock();
}
// 返回出队的元素
return result;
} private E dequeue() {
// 元素个数减1
int n = size - 1;
if (n < 0)
// 数组元素不足,返回null
return null;
else {
Object[] array = queue;
// 弹出堆顶元素
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);
// 修改size
size = n;
// 返回出队的元素
return result;
}
} 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;
// 取左右子节点中最小的值
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
// key如果比左右子节点都小,则堆化结束
if (key.compareTo((T) c) <= 0)
break;
// 否则,交换key与左右子节点中最小的节点的位置
array[k] = c;
k = child;
}
// 找到了放元素的位置,放置元素
array[k] = key;
}
}

出队的过程与PriorityQueue基本类似:

(1)加锁;

(2)判断是否出队成功,未成功就阻塞在notEmpty条件上;

(3)出队时弹出堆顶元素,并把堆尾元素拿到堆顶;

(4)再做自上而下的堆化;

(5)解锁;

总结

(1)PriorityBlockingQueue整个入队出队的过程与PriorityQueue基本是保持一致的;

(2)PriorityBlockingQueue使用一个锁+一个notEmpty条件控制并发安全;

(3)PriorityBlockingQueue扩容时使用一个单独变量的CAS操作来控制只有一个线程进行扩容;

(4)入队使用自下而上的堆化;

(5)出队使用自上而下的堆化;

彩蛋

为什么PriorityBlockingQueue不需要notFull条件?

因为PriorityBlockingQueue在入队的时候如果没有空间了是会自动扩容的,也就不存在队列满了的状态,也就是不需要等待通知队列不满了可以放元素了,所以也就不需要notFull条件了。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之PriorityBlockingQueue源码分析的更多相关文章

  1. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  2. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  3. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  4. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  5. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  6. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  7. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

  8. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

  9. 死磕 java集合之ConcurrentSkipListSet源码分析——Set大汇总

    问题 (1)ConcurrentSkipListSet的底层是ConcurrentSkipListMap吗? (2)ConcurrentSkipListSet是线程安全的吗? (3)Concurren ...

随机推荐

  1. Sublime text 3 注册码激活码 版本号3143

    —– BEGIN LICENSE —– TwitterInc 200 User License EA7E-890007 1D77F72E 390CDD93 4DCBA022 FAF60790 61AA ...

  2. 一些Gym三星单刷的比赛总结

    RDC 2013, Samara SAU ACM ICPC Quarterfinal Qualification Contest G 思路卡成智障呀! Round 1:对着这个魔法阵找了半天规律,效果 ...

  3. windows系统下输入法图标显示设置

    原先任务栏有两个搜狗输入法的标志,还有一个"中/英"的图标:甚至桌面还悬浮这一个搜狗输入法图标. 打开vscode等工具时,桌面悬浮的图标有时可能会遮挡到一些信息,十分不爽. 如今 ...

  4. web端创建地图

    1>在首部引入标签 <link rel="stylesheet" href="http://cache.amap.com/lbs/static/main111 ...

  5. 架构之微服务(zookeeper)

    ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等.Zookeeper是hadoop的一个子项目,其 ...

  6. Java开源生鲜电商平台-支付模块的设计与架构(源码可下载)

    Java开源生鲜电商平台-支付模块的设计与架构(源码可下载) 开源生鲜电商平台支付目前支持支付宝与微信.针对的是APP端(android or IOS)   1. 数据库表设计. 说明:无论是支付宝还 ...

  7. [CVPR2015] Is object localization for free? – Weakly-supervised learning with convolutional neural networks论文笔记

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px "Helvetica Neue"; color: #323333 } p. ...

  8. C++11 左值、右值、右值引用

    左值.右值 在C++11中所有的值必属于左值.右值两者之一,右值又可以细分为纯右值.将亡值.在C++11中可以取地址的.有名字的就是左值,反之,不能取地址的.没有名字的就是右值(将亡值或纯右值).举个 ...

  9. Redis数据结构简介

    Redis可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为STRING(字符串).LIST(列表).SET(集合).HASH(散列)和ZSET(有序集合).有一部分Redis命令对 ...

  10. PHP生成腾讯云COS请求签名

    目标 使用 PHP 创建 COS 接口所需要的请求签名 步骤 按照官方示例(也许是我笨,我怎么读都觉得官方文档结构费劲,示例细节互相不挨着,容易引起歧义),请求签名应用在需要身份校验的场景,即非公有读 ...