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实现原理源码详解的更多相关文章

  1. java延迟队列

    大多数用到定时执行的功能都是用任务调度来做的,单身当碰到类似订餐业务/购物等这种业务就不好处理了,比如购物的订单功能,在你的订单管理中有N个订单,当订单超过十分钟未支付的时候自动释放购物车中的商品,订 ...

  2. 延迟队列DelayQueue take() 源码分析

    延迟队列DelayQueue take() 源码分析 在工作中使用了延迟队列,对其内部的实现很好奇,于是就研究了一下其运行原理,在这里就介绍一下take()方法的源码 1 take()源码 如下所示 ...

  3. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

  4. RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息

    概述 在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分.本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现. ...

  5. Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j)

    Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j) 博客分类: Java综合   第一.Logger.getLogger()和Log ...

  6. “全栈2019”Java多线程第二十八章:公平锁与非公平锁详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. “全栈2019”Java多线程第二十二章:饥饿线程(Starvation)详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java第一百零四章:匿名内部类与外部成员互访详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  9. “全栈2019”Java第九十七章:在方法中访问局部内部类成员详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  10. “全栈2019”Java第九十二章:外部类与内部类成员覆盖详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

随机推荐

  1. Advanced .Net Debugging 2:CLR基础

    一.简介 这是2024新年后我的第一篇文章,也是我的<Advanced .Net Debugging>这个系列的第二篇文章.这篇文章告诉我们为了进行有效的程序调试,我们需要掌握哪些知识.言 ...

  2. Django多方式登录认证

    多方式认证原理 我们平常使用的Django登录认证是django.contrib.auth.authenticate 点进去源码,我们会看到这个函数,真正的认证类是它里面的那个 继续点进去源码,会看到 ...

  3. EF Invalid column name 'Discriminator' Invalid column name 'TagCode'.

    参考资料:Invalid column name 'TagCode'. 该异常和Discriminator没关系,一般原因:1.数据库中字段和实体类字段不一致导致的2.创建新增继承于数据库对应的实体类 ...

  4. 【LeetCode剑指offer 01】数组中重复的数字、两个栈实现队列

    数组中重复的数字 数组中重复的数字 找出数组中重复的数字. 在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数 ...

  5. 浅谈errgroup的使用以及源码分析

    本文讲解的是golang.org/x/sync这个包中的errgroup 1.errgroup 的基础介绍 学习过 Go 的朋友都知道 Go 实现并发编程是比较容易的事情,只需要使用go关键字就可以开 ...

  6. Emqx高可用架构

    目录 优化前架构 主要问题 haproxy问题 优化后架构 优化功能点 emq版本升级 linux系统调优 haproxy调优 测试工具 依赖安装 配置erl环境变量 安装压测软件 测试指令与结果展示 ...

  7. 【Azure 应用服务】应用代码中需要使用客户端证书访问服务接口,部署在应用服务后报错不能找到证书(Cannot find the X.509 certificate)

    问题描述 在应用中,需要访问另一个服务接口,这个接口需要使用客户端证书进行认证.在代码中使用 System.Security.Cryptography.X509Certificates 加载Windo ...

  8. C++ 模板的笔记1

    C++模板的笔记1 C++ 函数模板 函数模板的定义 函数模板是一种可以生成不同类型函数的函数声明.函数模板的参数类型不是固定的,而是在调用时由实参类型推导出来. 语法: template <t ...

  9. RocketMQ(10) 消息类型

    一.普通消息 1. 消息发送方式分类 Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果. 同步发送消息: 同步发送消息是指,Producer发出⼀条消息后,会在收到MQ返 ...

  10. windows编译ZLMediaKit流媒体服务webrtc

    环境说明 ZLMediaKit编译需要的软件 visual studio 2022 cmake 3.29.0-rc2 OpenSSL 1.1.1w(不想踩坑的话安装这个版本) libsrtp 2.6. ...