一、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. DVWA靶场Authorisation Bypass (未授权绕过) 漏洞通关教程及源码审计

    Authorisation Bypass 授权绕过(Authorisation Bypass)是一种严重的安全,通过利用系统的或错误配置,绕过正常的访问控制机制,获得未经授权的访问权限.这种可能导致敏 ...

  2. 牛客周赛 Round 77

    题目链接:牛客周赛 Round 77 A. 时间表 tag:签到 B. 数独数组 tag:签到 Description:给定n个数,每个数的范围为1-9,问能否经过排列,使其每个长度为9的连续子数组都 ...

  3. uniapp横向滚动

    scroll-x="true" 出现横向滚动 scroll-with-animation="true" 横向滚动有动画 <scroll-view clas ...

  4. 陶瓷电容(MLCC),你真的了解吗?

    摘要:本文主要介绍陶瓷电容(MLCC)的结构.阻抗-频率特性.直流偏压特性.温度特性和关键参数. 一.物理结构 多层片式陶瓷电容器(Multi-layer Ceramic Capacitor,MLCC ...

  5. MySQL8.0事务知识点

    mysql8.0事务学习 1.基本概念 事务(Transaction)是访问和更新数据库的程序执行单元:是一个最小的不可分割的工作单元,能保证一个业务的完整性:事务中可能包含一个或多个sql语句,这些 ...

  6. 烟草行业如何用低代码+ BI 实现数字化转型?

    在数字经济的大潮中,烟草行业正迎来重大的发展契机.国家层面的政策引导和战略规划为行业的数字化转型提供了明确的方向.<数字中国>的愿景逐步变为现实,国家信息化发展战略的深入推进,为烟草行业的 ...

  7. 洛谷P1983 [NOIP2013 普及组] 车站分级 题解

    思路 由题可知,在一趟车次的区间内,停靠的站点的等级恒大于不停靠的站点. 因此,对于每一趟车次的区间,给所有停靠的站点向所有不停靠的站点两两连有向边,然后求图中最长的路径长度,就能得到答案. 实现 因 ...

  8. [HDU4625] JZPTREE+[国家集训队] Crash 的文明世界 题解

    老师发福利,放了两道一毛一样的题. 考虑无视战术,直接化简: \[\sum_{v=1}^ndis(u,v)^k=\sum_{v=1}^n\sum_{i=0}^k\begin{Bmatrix}k\\i\ ...

  9. 从SQL Server迁移到Mysql Mysql导入SQL Server的数据库

    mysql怎么导入 SQl Server的数据库, SQL Server导出的SQL文件Mysql无法识别 需要用到的软件 Navicat mysql workbanch SQL Server man ...

  10. python进行大乐透和双色球选号(LSTM预测和随机选号)

    文章仅供参考学习 1.LSTM预测 首先去爬取数据 这个是爬取大乐透的,从07年爬到最新一期 import requests from bs4 import BeautifulSoup import ...