问题

(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. STL-Deque 源码剖析

    G++ ,cygnus\cygwin-b20\include\g++\stl_deque.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Comp ...

  2. Java 算法(二)

    [程序9] 题目:一个数如果恰好等于它的因子之和,这个数就称为"完数".例如6=1+2+3.编程找出1000以内的所有完数 //第一种public class A09 {publi ...

  3. 命令行备忘录 cli-memo

    前言 有时候想用一个简洁点儿的备忘录,发现没有简洁好用的,于是就想着开发一个,秉着简洁 的思想,所以连界面都没有,只能通过命令行来操作(尽可能的将命令简化).设计的时候 借鉴了git分支的思想,每个备 ...

  4. Mysql访问权限问题:Access denied for user 'root'@'XXX' (using password: YES)

    System.Data.Entity.Core.ProviderIncompatibleException: An error occurred accessing the database. Thi ...

  5. hibernate多表查询封装实体

    以前用sql实现联合查询 是非常简单的事,只需要写sql语句就可以,第一次遇到hibernate要实现多表联合查询的时候还楞了一下.最后看了下资料,才恍然大悟,hibernate实现多表联合查询跟SQ ...

  6. Selenium自动化测试-unittest单元测试框架

    一.Pyhon工作原理-- 核心概念:test case, testsuite, TestLoder,TextTestRunner,TextTestResult, test fixture TestC ...

  7. vector的内存分配问题

    vector的内存增长问题,其实无非是vector中size()和capacity()问题.vector的一个缺点就是它的内存分配是按照2的倍数分配内存的.当当前容量对插入元素不够时,分配一块新的内存 ...

  8. Linux时间子系统之(十三):Tick Device layer综述

    专题文档汇总目录 Notes:从概念层次描述了tick-comm.oneshot tick.broadcast tick:重点介绍了tick和tickless概念及其区别,两种tick device: ...

  9. SpringData ES中一些底层原理的分析

    之前写过一篇SpringData ES 关于字段名和索引中的列名字不一致导致的查询问题,顺便深入学习下Spring Data Elasticsearch. Spring Data Elasticsea ...

  10. Hot swapping

    83. Hot swapping83.1 Reload static contentThere are several options for hot reloading. The recommend ...