一、DelayQueue 底层原理

DelayQueue是一种本地延迟队列,比如希望我们的任务在5秒后执行,就可以使用DelayQueue实现。常见的使用场景有:

  • 订单10分钟内未支付,就取消。

  • 缓存过期后,就删除。

  • 消息的延迟发送等。

但是DelayQueue是怎么使用的?底层原理是什么样的?如果有多个任务是怎么排队的? 看完这篇文章,可以轻松解答这些问题。

由于DelayQueue实现了BlockingQueue接口,而BlockingQueue接口中定义了几组放数据和取数据的方法,来满足不同的场景。

二、DelayQueue类结构

先看一下DelayQueue类里面有哪些属性:

  public class DelayQueue<E extends Delayed>
extends AbstractQueue<E>
implements BlockingQueue<E> { /**
* 排它锁,用于保证线程安全
*/
private final transient ReentrantLock lock = new ReentrantLock(); /**
* 底层是基于PriorityQueue实现
*/
private final PriorityQueue<E> q = new PriorityQueue<E>(); /**
* 当前线程
*/
private Thread leader = null; /**
* 条件队列
*/
private final Condition available = lock.newCondition(); }

DelayQueue实现了BlockingQueue接口,是一个阻塞队列。并且DelayQueue里面的元素需要实现Delayed接口。使用了ReentrantLock保证线程安全,使用了Condition作条件队列,当队列中没有过期元素的时候,取数据的线程需要在条件队列中等待。

public interface Delayed extends Comparable<Delayed> {

    /**
* 返回剩余过期时间
*/
long getDelay(TimeUnit unit);
}

1、初始化

DelayQueue常用的初始化方法有两个,无参构造方法和指定元素集合的有参构造方法。

/**
* 无参构造方法
*/
public DelayQueue() {
} /**
* 指定元素集合
*/
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}

2、使用示例

先定义一个延迟任务,需要实现Delayed接口,并重写getDelay()和compareTo()方法。

  public class DelayedTask implements Delayed {

      /**
* 任务到期时间
*/
private long expirationTime; /**
* 任务
*/
private Runnable task; public void execute() {
task.run();
} public DelayedTask(long delay, Runnable task) {
// 到期时间 = 当前时间 + 延迟时间
this.expirationTime = System.currentTimeMillis() + delay;
this.task = task;
} /**
* 返回延迟时间
*/
@Override
public long getDelay(@NotNull TimeUnit unit) {
return unit.convert(expirationTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
} /**
* 任务列表按照到期时间排序
*/
@Override
public int compareTo(@NotNull Delayed o) {
return Long.compare(this.expirationTime, ((DelayedTask) o).expirationTime);
}
}

测试运行延迟任务

  @Slf4j
public class DelayQueueTest { public static void main(String[] args) throws InterruptedException {
// 初始化延迟队列
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>(); // 添加3个任务,延迟时间分别是3秒、1秒、5秒
delayQueue.add(new DelayedTask(3000, () -> log.info("任务2开始运行")));
delayQueue.add(new DelayedTask(1000, () -> log.info("任务1开始运行")));
delayQueue.add(new DelayedTask(5000, () -> log.info("任务3开始运行"))); // 运行任务
log.info("开始运行任务");
while (!delayQueue.isEmpty()) {
//阻塞获取最先到期的任务
DelayedTask task = delayQueue.take();
task.execute();
}
}
}

输出结果:

可以看出,运行任务的时候,会按照任务的到期时间进行排序,先到期的任务先运行。如果没有到期的任务,调用take()方法的时候,会一直阻塞。

三、offer方法源码

先看一下offer()方法源码,其他放数据方法逻辑也是大同小异。 offer()方法在队列满的时候,会直接返回false,表示插入失败。

/**
* offer方法入口
*
* @param e 元素
* @return 是否插入成功
*/
public boolean offer(E e) {
// 1. 获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 2. 直接调用PriorityQueue的offer方法
q.offer(e);
// 3. 如果是第一次放数据,需要唤醒调用take方法阻塞的线程
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
// 4. 释放锁
lock.unlock();
}
}

DelayQueue的offer()方法底层是基于PriorityQueue的offer()方法实现的,而PriorityQueue内部实现了排序任务的功能, 由于PriorityQueue的offer()方法实现了队列自动扩容,所以正常情况都会添加数据成功。

四、add方法源码

add()方法底层基于offer()实现,逻辑相同。

/**
* add方法入口
*
* @param e 元素
* @return 是否添加成功
*/
public boolean add(E e) {
return offer(e);
}

五、put方法源码

put()方法底层也是基于offer()实现,逻辑相同。

/**
* put方法入口
*
* @param e 元素
*/
public void put(E e) {
offer(e);
}

六、poll方法源码

看一下poll()方法源码,其他方取数据法逻辑大同小异,都是从链表头部弹出元素。 poll()方法在弹出元素的时候,如果队列为空,直接返回null,表示弹出失败。

  /**
* poll方法入口
*/
public E poll() {
// 1. 获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 2. 获取队头元素
E first = q.peek();
// 3. 如果队头元素为空,或者还没有过期,则返回null
if (first == null || first.getDelay(NANOSECONDS) > 0) {
return null;
} else {
// 5. 否则返回队头元素
return q.poll();
}
} finally {
// 4. 释放锁
lock.unlock();
}
}

七、remove方法源码

再看一下remove()方法源码,如果队列为空,remove()会抛出异常

/**
* remove方法入口
*/
public E remove() {
// 1. 直接调用poll方法
E x = poll();
// 2. 如果取到数据,直接返回,否则抛出异常
if (x != null) {
return x;
} else {
throw new NoSuchElementException();
}
}

八、take方法源码

再看一下take()方法源码,如果队列为空,take()方法就一直阻塞,直到被唤醒。

  public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); // 获取队列头部元素
if (first == null) {
available.await(); // 如果队列为空,阻塞等待
} else {
long delay = first.getDelay(NANOSECONDS); // 获取剩余延迟时间
if (delay <= 0) {
return q.poll(); // 如果元素已到期,移除并返回
}
first = null; // 释放引用,避免内存泄漏
if (leader != null) {
available.await(); // 如果有其他线程在等待,当前线程阻塞
} else {
Thread thisThread = Thread.currentThread();
leader = thisThread; // 设置当前线程为 leader
try {
available.awaitNanos(delay); // 阻塞直到元素到期
} finally {
if (leader == thisThread) {
leader = null; // 重置 leader
}
}
}
}
}
} finally {
if (leader == null && q.peek() != null) {
available.signal(); // 唤醒其他等待的线程
}
lock.unlock();
}
}

当队列为空的时候,take()方法会一直阻塞,所以需要加可中断的锁。take()方法逻辑也很简单,就是判断队头元素,如果队头元素存在并且已到期,直接返回,否则就阻塞等待

DelayQueue 底层原理的更多相关文章

  1. Neo4j图数据库简介和底层原理

    现实中很多数据都是用图来表达的,比如社交网络中人与人的关系.地图数据.或是基因信息等等.RDBMS并不适合表达这类数据,而且由于海量数据的存在,让其显得捉襟见肘.NoSQL数据库的兴起,很好地解决了海 ...

  2. 【T-SQL进阶】02.理解SQL查询的底层原理

    本系列[T-SQL]主要是针对T-SQL的总结. [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础]02.联接查询 [T-SQL基础]03.子查询 [T-SQL基础]04.表表达式 ...

  3. spring框架的IOC的底层原理

    1.IOC概念:spring容器创建对象并管理 2.IOC的底层原理的具体实现: 1)所使用的技术: (1). dom4j解析xml配置文件 (2).工厂设计模式(解耦合) (3).反射 第一步:配置 ...

  4. 深入研究Sphinx的底层原理和高级使用

    深入研究Sphinx的底层原理和高级使用

  5. 深入研究Node.js的底层原理和高级使用

    深入研究Node.js的底层原理和高级使用

  6. HashMap的底层原理

    简单说: 底层原理就是采用数组加链表: 两张图片很清晰地表明存储结构: 既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现: // 存储时: int hash = ke ...

  7. 操作系统底层原理与Python中socket解读

    目录 操作系统底层原理 网络通信原理 网络基础架构 局域网与交换机/网络常见术语 OSI七层协议 TCP/IP五层模型讲解 Python中Socket模块解读 TCP协议和UDP协议 操作系统底层原理 ...

  8. Servlet底层原理、Servlet实现方式、Servlet生命周期

    Servlet简介 Servlet定义 Servlet是一个Java应用程序,运行在服务器端,用来处理客户端请求并作出响应的程序. Servlet的特点 (1)Servlet对像,由Servlet容器 ...

  9. Spring Aop底层原理详解

    Spring Aop底层原理详解(来源于csdn:https://blog.csdn.net/baomw)

  10. JavaScript是如何工作的: CSS 和 JS 动画底层原理及如何优化它们的性能

    摘要: 理解浏览器渲染. 原文:JavaScript是如何工作的: CSS 和 JS 动画底层原理及如何优化它们的性能 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 这是专门探索 J ...

随机推荐

  1. Collection的子接口之一:List 接口

    List 接口概述: 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组 List集合类中 元素有序.且可重复,集合中的每个元素都有其对应的顺序索引. List容器中的元素都对 ...

  2. ef 值转换与值比较器

    前言 简单介绍一下,值转换器和值比较器. 正文 为什么有值转换器这东西呢? 那就是这个东西一直必须存在. 比如说,我们的c# enum 对应数据库的什么呢? 是int还是string呢? 一般情况下, ...

  3. idea操作小技巧总结

    一.热键 光标导航前进|后退 Ctrl+Alt+左右方向键 光标转到语句块的头尾 Ctrl+[|] 再次提示函数参数列表 Ctrl+P 插入实时模板 Ctrl+J 文件结构查看 Ctrl+F12 书签 ...

  4. Oracle连接 ORA-28001: 口令已经失效解决方法

    cmd进入命令行 C:\Users\Administrator>sqlplus / as sysdba SQL*Plus: Release 11.2.0.1.0 Production on 星期 ...

  5. C#反射机制学习

    参考原文链接:https://blog.csdn.net/xiaouncle/java/article/details/52983924 反射是.NET中的重要机制,通过反射可以得到*.exe或*.d ...

  6. @Scheduled参数及cron表达式解释

    @Scheduled支持以下8个参数:1.cron:表达式,指定任务在特定时间执行:2.fixedDelay:表示上一次任务执行完成后多久再次执行,参数类型为long,单位ms:3.fixedDela ...

  7. 最小费用最大流问题的 SSP 算法

    我们已经了解最大流问题,其目标是通过网络中的各条边传输流量,尽可能地从源点流向汇点.通过经典的算法,如 Ford-Fulkerson 增广,我们能够找到一种方式,最大化从源点到汇点的流量. 然而,最大 ...

  8. 用于敏捷开发的最佳免费 UML 工具 2022

    Table of Contents  hide  1 最好的在线免费 UML图工具 2 免费的 UML Visual Paradigm 在线平台 3 其他福利 4 用于正式和大规模可视化建模的 Vis ...

  9. Elasticsearch搜索引擎学习笔记(一)

    核心概念 ES -> 数据库 索引index -> 表 文档 document -> 行(记录) 字段 fields -> 列 安装Elasticsearch 1. 上传后解压 ...

  10. 展开说说关于C#中ORM框架的用法!

    Entity Framework(EF)是微软提供的一个开源的对象关系映射(ORM)框架,用于.NET应用程序.它经历了多个版本的演进,主要分为两大分支:Entity Framework 6 (EF6 ...