阿里面试官:你了解过延迟队列DelayQueue的底层实现原理吗?

引言

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

  • 订单10分钟内未支付,就取消。
  • 缓存过期后,就删除。
  • 消息的延迟发送等。

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

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

操作抛出异常返回特定值一直阻塞阻塞指定时间
放数据add()offer()put()offer(e, time, unit)
取数据(同时删除数据)remove()poll()take()poll(time, unit)
取数据(不删除)element()peek()不支持不支持

这四组方法的区别是:

  1. 当队列满的时候,再次添加数据,add()会抛出异常,offer()会返回false,put()会一直阻塞,offer(e, time, unit)会阻塞指定时间,然后返回false。
  2. 当队列为空的时候,再次取数据,remove()会抛出异常,poll()会返回null,take()会一直阻塞,poll(time, unit)会阻塞指定时间,然后返回null。

类结构

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

 

java

复制代码

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(); }

image.png

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

 

java

复制代码

public interface Delayed extends Comparable<Delayed> { /** * 返回剩余过期时间 */ long getDelay(TimeUnit unit); }

初始化

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

 

java

复制代码

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

使用示例

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

 

java

复制代码

/** * @author 一灯架构 * @apiNote 自定义延迟任务 **/ 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); } }

测试运行延迟任务

 

java

复制代码

/** * @author 一灯架构 * @apiNote DelayQueue测试类 **/ @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(); } } }

输出结果:

 

java

复制代码

10:30:10.000 [main] INFO com.yideng.DelayQueueTest - 开始运行任务 10:30:11.000 [main] INFO com.yideng.DelayQueueTest - 任务1开始运行 10:30:13.000 [main] INFO com.yideng.DelayQueueTest - 任务2开始运行 10:30:15.000 [main] INFO com.yideng.DelayQueueTest - 任务3开始运行

可以看出,运行任务的时候,会按照任务的到期时间进行排序,先到期的任务先运行。如果没有到期的任务,调用take()方法的时候,会一直阻塞。 然后再看一下源码实现,先看放数据的几组方法。

放数据源码

放数据的方法有四个:

操作抛出异常返回特定值阻塞阻塞一段时间
放数据add()offer()put()offer(e, time, unit)

offer方法源码

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

 

java

复制代码

/** * 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()实现,逻辑相同。

 

java

复制代码

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

put方法源码

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

 

java

复制代码

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

offer(e, time, unit)源码

offer(e, time, unit)方法底层也是基于offer()实现,逻辑相同,并没有实现阻塞指定时间的功能,设计源码的这帮人也留了一堆坑「尴尬」。

 

java

复制代码

/** * offer方法入口 * * @param e 元素 * @param timeout 超时时间 * @param unit 时间单位 * @return 是否添加成功 */ public boolean offer(E e, long timeout, TimeUnit unit) { return offer(e); }

弹出数据源码

弹出数据(取出数据并删除)的方法有四个:

操作抛出异常返回特定值阻塞阻塞一段时间
取数据(同时删除数据)remove()poll()take()poll(time, unit)

poll方法源码

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

 

java

复制代码

/** * 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()会抛出异常。

 

java

复制代码

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

take方法源码

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

 

java

复制代码

/** * take方法入口 */ public E take() throws InterruptedException { // 1. 加锁,加可中断的锁 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { // 2. 获取队头元素 E first = q.peek(); // 3. 如果队头元素为null,则阻塞等待 if (first == null) { available.await(); } else { // 4. 如果队头元素不为null,则获取队头元素延迟时间 long delay = first.getDelay(NANOSECONDS); // 5. 如果延迟时间小于等于0,表示已到期,则返回队头元素 if (delay <= 0) { return q.poll(); } first = null; // 6. 如果队头元素未到期,则阻塞等待 if (leader != null) { 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(); } // 7. 释放锁 lock.unlock(); } }

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

poll(time, unit)源码

再看一下poll(time, unit)方法源码,在队列满的时候, poll(time, unit)方法会阻塞指定时间,然后然后null。

 

java

复制代码

/** * poll方法入口 * * @param timeout 超时时间 * @param unit 时间单位 * @return 元素 */ public E poll(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); // 1. 加锁,加可中断的锁 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (; ; ) { // 2. 获取队头元素 E first = q.peek(); // 3. 如果队头元素为null,则阻塞等待 if (first == null) { if (nanos <= 0) { return null; } else { nanos = available.awaitNanos(nanos); } } else { // 4. 如果队头元素不为null,则获取队头元素延迟时间 long delay = first.getDelay(NANOSECONDS); // 5. 如果延迟时间小于等于0,表示已到期,则返回队头元素 if (delay <= 0) { return q.poll(); } if (nanos <= 0) { return null; } first = null; // 6. 如果队头元素未到期,则阻塞等待 if (nanos < delay || leader != null) { nanos = available.awaitNanos(nanos); } else { Thread thisThread = Thread.currentThread(); leader = thisThread; try { long timeLeft = available.awaitNanos(delay); nanos -= delay - timeLeft; } finally { if (leader == thisThread) leader = null; } } } } } finally { if (leader == null && q.peek() != null) available.signal(); // 7. 释放锁 lock.unlock(); } }

poll(time, unit)与上面的take()方法逻辑类似,区别是,当队列为空的时候,take()方法会一直阻塞,poll(time, unit)方法只会阻塞指定时间。

查看数据源码

再看一下查看数据源码,查看数据,并不删除数据。

操作抛出异常返回特定值阻塞阻塞一段时间
取数据(不删除)element()peek()不支持不支持

peek方法源码

先看一下peek()方法源码,如果数组为空,直接返回null,底层基于PriorityQueue的peek()方法实现。

 

java

复制代码

/** * peek方法入口 */ public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { return q.peek(); } finally { lock.unlock(); } }

element方法源码

再看一下element()方法源码,如果队列为空,则抛出异常,底层直接使用的peek()方法。

 

java

复制代码

/** * element方法入口 */ public E element() { // 1. 调用peek方法查询数据 E x = peek(); // 2. 如果查到数据,直接返回 if (x != null) { return x; } else { // 3. 如果没找到,则抛出异常 throw new NoSuchElementException(); } }

总结

这篇文章讲解了DelayQueue阻塞队列的核心源码,了解到DelayQueue队列具有以下特点:

  1. DelayQueue实现了BlockingQueue接口,提供了四组放数据和读数据的方法,来满足不同的场景。
  2. DelayQueue底层采用组合的方式,复用PriorityQueue的按照延迟时间排序任务的功能,实现了延迟队列。
  3. DelayQueue是线程安全的,内部使用ReentrantLock加锁。

今天一起分析了DelayQueue队列的源码,可以看到DelayQueue的源码非常简单,没有什么神秘复杂的东西,下篇文章再一起接着分析其他的阻塞队列源码。

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
延迟队列DelayQueue)是 Java 并发包中提供的一种队列实现,用于存储具有延迟时间的元素。延迟队列中的元素只有在其指定的延迟时间过去后才能被取出。 下面是一个简单的延迟队列的 Java 实现示例: 首先,我们需要定义一个实现了 Delayed 接口的延迟元素类: ```java import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayedElement implements Delayed { private String data; private long startTime; public DelayedElement(String data, long delay) { this.data = data; this.startTime = System.currentTimeMillis() + delay; } @Override public long getDelay(TimeUnit unit) { long diff = startTime - System.currentTimeMillis(); return unit.convert(diff, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed other) { if (this.startTime < ((DelayedElement) other).startTime) { return -1; } if (this.startTime > ((DelayedElement) other).startTime) { return 1; } return 0; } // Getter and Setter methods } ``` 然后,我们可以使用 DelayQueue 来存储延迟元素,并在需要时取出元素: ```java import java.util.concurrent.DelayQueue; public class DelayQueueExample { public static void main(String[] args) throws InterruptedException { DelayQueue<DelayedElement> delayQueue = new DelayQueue<>(); // 添加延迟元素到队列 delayQueue.add(new DelayedElement("Element 1", 2000)); // 延迟 2 秒 delayQueue.add(new DelayedElement("Element 2", 5000)); // 延迟 5 秒 delayQueue.add(new DelayedElement("Element 3", 1000)); // 延迟 1 秒 // 取出延迟元素并处理 while (!delayQueue.isEmpty()) { DelayedElement element = delayQueue.take(); System.out.println("Processing element: " + element.getData()); } } } ``` 在上面的示例中,我们创建了一个 DelayQueue 对象,并向其中添加了三个延迟元素。然后,我们使用 `take()` 方法从队列中取出元素,并进行处理。如果队列为空,`take()` 方法会阻塞等待,直到有延迟元素可用。 请注意,延迟时间的单位可以根据需要进行调整,例如使用 `TimeUnit.SECONDS` 表示秒。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值