Java DelayQueue 介绍(一)-Java专区论坛-技术-SpringForAll社区

Java DelayQueue 介绍(一)

DelayQueue 是 JUC 包(java.util.concurrent)为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 BlockingQueue 的一种,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的。

BlockingQueue 的实现类

DelayQueue 中存放的元素必须实现 Delayed 接口,并且需要重写 getDelay()方法(计算是否到期)。

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

默认情况下, DelayQueue 会按照到期时间升序编排任务。只有当元素过期时(getDelay()方法返回值小于等于 0),才能从队列中取出。

DelayQueue 发展史

  • DelayQueue 最早是在 Java 5 中引入的,作为 java.util.concurrent 包中的一部分,用于支持基于时间的任务调度和缓存过期删除等场景,该版本仅仅支持延迟功能的实现,还未解决线程安全问题。
  • 在 Java 6 中,DelayQueue 的实现进行了优化,通过使用 ReentrantLock 和 Condition 解决线程安全及线程间交互的效率,提高了其性能和可靠性。
  • 在 Java 7 中,DelayQueue 的实现进行了进一步的优化,通过使用 CAS 操作实现元素的添加和移除操作,提高了其并发操作性能。
  • 在 Java 8 中,DelayQueue 的实现没有进行重大变化,但是在 java.time 包中引入了新的时间类,如 Duration 和 Instant,使得使用 DelayQueue 进行基于时间的调度更加方便和灵活。
  • 在 Java 9 中,DelayQueue 的实现进行了一些微小的改进,主要是对代码进行了一些优化和精简。

总的来说,DelayQueue 的发展史主要是通过优化其实现方式和提高其性能和可靠性,使其更加适用于基于时间的调度和缓存过期删除等场景。

DelayQueue 常见使用场景示例

我们这里希望任务可以按照我们预期的时间执行,例如提交 3 个任务,分别要求 1s、2s、3s 后执行,即使是乱序添加,1s 后要求 1s 执行的任务会准时执行。延迟任务

对此我们可以使用 DelayQueue 来实现,所以我们首先需要继承 Delayed 实现 DelayedTask,实现 getDelay 方法以及优先级比较 compareTo

/**
 * 延迟任务
 */
public class DelayedTask implements Delayed {
    /**
     * 任务到期时间
     */
    private long executeTime;
    /**
     * 任务
     */
    private Runnable task;

    public DelayedTask(long delay, Runnable task) {
        this.executeTime = System.currentTimeMillis() + delay;
        this.task = task;
    }

    /**
     * 查看当前任务还有多久到期
     * @param unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 延迟队列需要到期时间升序入队,所以我们需要实现compareTo进行到期时间比较
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.executeTime, ((DelayedTask) o).executeTime);
    }

    public void execute() {
        task.run();
    }
}

完成任务的封装之后,使用就很简单了,设置好多久到期然后将任务提交到延迟队列中即可。

// 创建延迟队列,并添加任务
DelayQueue < DelayedTask > delayQueue = new DelayQueue < > ();

//分别添加1s、2s、3s到期的任务
delayQueue.add(new DelayedTask(2000, () -> System.out.println("Task 2")));
delayQueue.add(new DelayedTask(1000, () -> System.out.println("Task 1")));
delayQueue.add(new DelayedTask(3000, () -> System.out.println("Task 3")));

// 取出任务并执行
while (!delayQueue.isEmpty()) {
  //阻塞获取最先到期的任务
  DelayedTask task = delayQueue.take();
  if (task != null) {
    task.execute();
  }
}

从输出结果可以看出,即使笔者先提到 2s 到期的任务,1s 到期的任务 Task1 还是优先执行的。

Task 1
Task 2
Task 3

DelayQueue 常见问题题

DelayQueue 的实现原理是什么?

DelayQueue 底层是使用优先队列 PriorityQueue 来存储元素,而 PriorityQueue 采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得 DelayQueue 对于延迟任务优先级的管理就变得十分方便了。同时 DelayQueue 为了保证线程安全还用到了可重入锁 ReentrantLock,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,DelayQueue 还用到了 Condition,通过 Condition 的 await 和 signal 方法完成多线程之间的等待唤醒。

DelayQueue 的实现是否线程安全?

DelayQueue 的实现是线程安全的,它通过 ReentrantLock 实现了互斥访问和 Condition 实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。

DelayQueue 的使用场景有哪些?

DelayQueue 通常用于实现定时任务调度和缓存过期删除等场景。在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 DelayQueue 中,DelayQueue 会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 DelayQueue 中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。

DelayQueue 中 Delayed 接口的作用是什么?

Delayed 接口定义了元素的剩余延迟时间(getDelay)和元素之间的比较规则(该接口继承了 Comparable 接口)。若希望元素能够存放到 DelayQueue 中,就必须实现 Delayed 接口的 getDelay() 方法和 compareTo() 方法,否则 DelayQueue 无法得知当前任务剩余时长和任务优先级的比较。

DelayQueue 和 Timer/TimerTask 的区别是什么?

DelayQueue 和 Timer/TimerTask 都可以用于实现定时任务调度,但是它们的实现方式不同。DelayQueue 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 Timer/TimerTask 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,DelayQueue 还支持动态添加和移除任务,而 Timer/TimerTask 只能在创建时指定任务。

请登录后发表评论

    没有回复内容