java延迟队列DelayQueue及底层优先队列PriorityQueue实现原理源码详解
DelayQueue是基于java中一个非常牛逼的队列PriorityQueue(优先队列),PriorityQueue是java1.5新加入的,当我看到Doug Lea大神的署名之后,我就知道这个队列不简单,那我们先来看一下他的源码吧:
作为一个队列来说,最基础的就是新增和查询,首先我们看下入队的逻辑:
1.入队
PriorityQueue提供了offer方法新增元素(add方法其实也是offer实现的),我们直接看下源码:
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
offer方法首先判断是否需要扩容,若需要则走grow方法:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
当长度小于64,扩容一倍+2,否则扩容50%。
再往下看若队列中没有元素,直接复制下标为0的元素,否则调用siftUp方法:
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
俩方法差不多一个,具体可搜compare和compareTo的区别如:https://blog.csdn.net/fly910905/article/details/81670353,我们直接看siftUpComparable方法:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}

结合上图和代码可以看出每个节点新增时,首先会根据节点下标计算出当前新节点应该属于的节点的父节点,比较当小于父节点则交换,无限循环,知道不存在父节点或者当前节点大于父节点的值,这样可以保证每个节点都比起子节点要小。
2.出队
入队的时候基本都差不多,但出队却有好几种,我们首先看peek方法:
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
代码简洁明了,就是查询出第一个,这只能算查询,算不上出队,我觉得应该叫点名。
再看poll方法:
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek(); //取第一个节点
if (first == null || first.getDelay(NANOSECONDS) > 0) //节点为空或者首节点未到延时时间直接返回null
return null;
else
return q.poll(); //PriorityQueue取节点逻辑
} finally {
lock.unlock();
}
}
再看PriorityQueue.poll方法:
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
首先取出第一个节点,然后将最后一个节点放替换首节点,并与子节点对比找出最小的并替换直到当前节点为最小为止,具体替换流程见siftDown代码:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least 拿到左子节点下标
Object c = queue[child];
int right = child + 1; //右子节点下标
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right]; //取出左右节点较小的
if (key.compareTo((E) c) <= 0) //当前节点比子节点小,结束流程
break;
queue[k] = c; //替换子节点至父节点
k = child;
}
queue[k] = key;
}
这个代码看起来稍微复杂点,会首先拿到左子节点和右子节点,对比取出较小的节点后与当前节点对比,将小的放在父节点位置,其实这里也是保证替换后的节点依然保持每个父节点最小,符合小顶堆。具体流程如下图所示:


我们最后看下take方法的实现:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); //取出第一个节点
if (first == null) //首节点为空说明队列为空,await等待
available.await();
else { //说明队列中有节点
long delay = first.getDelay(NANOSECONDS); //获取首节点延时时间
if (delay <= 0) //延时时间到期,直接取
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null) //说明当前有其他线程在操作(一般是其他线程在await)
available.await();
else { //这里设置操作线程为自己,并等待延时时间时长
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
这个实现一看就是阻塞式等待,取不到不罢休系列。
总结:
这篇写的还是比较简单的,大体介绍了DelayQueue的实现,也从底层了解了小顶堆PriorityQueue的实现,算是补充了之前对延时队列的具体实现,这篇主要是通过一个小顶堆的实现,保证每次取得值都是最小的,而又不用像数组那样每次插入都要重新排序,这里只要排序一部分就可以,也保证了性能,而DelayQueue中,加入了ReentrantLock保证了多线程的线程安全,同时加入Condition实现了延时阻塞式存取的机制,jdk的代码还是牛,这里其实就是我之前写锁的时候介绍的等待通知模式的一种实现,结合起来看还是有一些收获的。
java延迟队列DelayQueue及底层优先队列PriorityQueue实现原理源码详解的更多相关文章
- java延迟队列
大多数用到定时执行的功能都是用任务调度来做的,单身当碰到类似订餐业务/购物等这种业务就不好处理了,比如购物的订单功能,在你的订单管理中有N个订单,当订单超过十分钟未支付的时候自动释放购物车中的商品,订 ...
- 延迟队列DelayQueue take() 源码分析
延迟队列DelayQueue take() 源码分析 在工作中使用了延迟队列,对其内部的实现很好奇,于是就研究了一下其运行原理,在这里就介绍一下take()方法的源码 1 take()源码 如下所示 ...
- RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列
概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...
- RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息
概述 在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分.本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现. ...
- Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j)
Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j) 博客分类: Java综合 第一.Logger.getLogger()和Log ...
- “全栈2019”Java多线程第二十八章:公平锁与非公平锁详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第二十二章:饥饿线程(Starvation)详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java第一百零四章:匿名内部类与外部成员互访详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第九十七章:在方法中访问局部内部类成员详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第九十二章:外部类与内部类成员覆盖详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
随机推荐
- 【LeetCode栈与队列#04】逆波兰表达式求值(仍然是经典的栈操作)
逆波兰表达式求值 力扣题目链接(opens new window) 根据 逆波兰表示法,求表达式的值. 有效的运算符包括 + , - , * , / .每个运算对象可以是整数,也可以是另一个逆波兰表达 ...
- 【Azure Developer】在使用中国区 Azure AD B2C时, AUTHORITY的值是什么呢?
问题描述 使用MSAL4J的SDK调用(源码地址:https://github.com/Azure-Samples/ms-identity-msal-java-samples/tree/main/3. ...
- Java 递归方法的使用 + 例子
1 /* 2 * 递归方法的使用 3 * 1.递归方法:一个方法体内调用它自身 4 * 2.方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制 5 * 递归一定要想已知方向 ...
- 使用Kubernetes搭建带有ik分词的Elasticsearch集群
创建好带有Ik分词的es镜像,并上传到镜像仓库中,创建镜像可参考链接中的文档 https://www.cnblogs.com/hi-lijq/p/16895206.html 编写es_cluster- ...
- SQL注入详细讲解概括—盲注
SQL注入详细讲解概括-盲注 1.盲注简单理解 2.盲注必学函数 3.布尔盲注 4.时间盲注 一.盲注简单理解 What is 盲注? It is 在服务器没有错误回显的时候完成的注入攻击 数据库把报 ...
- Linux DISPLAY环境变量的妙用(error:QXcbConnection: Could not connect to display) ,xhost 命令, 通过ssh连接显示界面
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ...
- 三维模型3DTile格式轻量化云端处理技术方法分析
三维模型3DTile格式轻量化云端处理技术方法分析 在现代的地理信息系统 (GIS) 中,3D Tiles 是一种很重要的数据格式,用于存储和传输大规模地理空间数据.然而,由于其数据密度高,传输和加载 ...
- 恶意软件开发(三)经典DLL注入流程
什么是dll注入? DLL注入允许将外部DLL文件加载到进程中并运行其中的代码.DLL(动态链接库)是一种可重用的代码库,它包含在多个程序中使用的函数.类.变量和其他程序代码.DLL注入技术可以通过将 ...
- 记录--webpack和vite原理
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 每次用vite创建项目秒建好,前几天用vue-cli创建了一个项目,足足等了我一分钟,那为什么用 vite 比 webpack 要快 ...
- 记录-因为写不出拖拽移动效果,我恶补了一下Dom中的各种距离
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 背景 最近在项目中要实现一个拖拽头像的移动效果,一直对JS Dom拖拽这一块不太熟悉,甚至在网上找一个示例,都看得云里雾里的,发现遇到最大 ...