天天看点

一文读懂,Java内置的延迟队列DelayQueue,原理及使用方法

作者:Code404

Java的延迟队列(DelayQueue)是一种带有延迟时间的阻塞队列,最初在JDK1.5中引入。它允许我们向队列中添加具有延迟时间的元素,并在元素到期后从队列中获取这些元素。

一、实现原理

Java 延迟队列的实现基于 priority queue (优先级队列),队列中的元素根据到期时间排序。队列头部是最先到期的元素,每个元素都可以有不同的到期时间。DelayQueue 内部使用了堆排序算法,因此可以快速高效地查找、插入和删除元素。即使队列中的元素数量非常庞大,它的性能也不会受到影响。

当添加一个元素到队列中时,该元素会按照其到期时间插入到适当的位置,排成有序序列。当队列中的元素到达到期时间时,该元素会从队列头部被移除。

每个元素都是一个实现了 java.util.concurrent.Delayed 接口的对象,该接口定义了两个方法:

long getDelay(TimeUnit unit); // 返回该元素还需等待的时间。
int compareTo(Delayed o); // 对元素进行比较,以便于维护过期元素的顺序。           

Java 延迟队列 DelayQueue 实现了 BlockingQueue 接口,因此它具备了阻塞等待的能力,当尝试取出一个元素时,如果队列中没有到期的元素,那么当前线程会进入阻塞状态,直到有一个元素过期或者插入一个过期的元素,唤醒当前线程进行取出操作。

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    ......
    
}           

二、代码示例

定义了一个名为 DelayedElement 的类,它包含一个延迟时间和一个字符串 data。在构造函数中,我们计算出需要等待的时间,并将延迟时间设置为当前时间加上该时间。

实现了 Delayed 接口之后,我们需要实现 getDelay 和 compareTo 方法。 getDelay 方法返回元素还需要等待的时间, compareTo 方法用于比较两个元素的优先级,其中优先级由剩余的延迟时间决定。

public class DelayedElement implements Delayed {
     // 元素的到期时间
    private long delayTime;
    // 元素包含的数据
    private String data; 

    /**
     * 创建一个带有延迟时间的元素对象
     * @param delayTime 延迟时间,单位为毫秒
     * @param data 包含的数据
     */
    public DelayedElement(long delayTime, String data) {
       // 计算元素的到期时间
        this.delayTime = System.currentTimeMillis() + delayTime; 
        this.data = data;
    }

    /**
     * 获取元素的剩余延迟时间
     * @param unit 时间单位
     * @return 元素的剩余延迟时间
     */
    @Override
    public long getDelay(TimeUnit unit) {
        // 计算元素到期时间与当前时间的时间差
        long diff = delayTime - System.currentTimeMillis();
        // 将时间差转换为指定的时间单位
        return unit.convert(diff, TimeUnit.MILLISECONDS); 
    }

    /**
     * 比较两个元素的到期时间,用于优先级队列的排序
     * @param o 要比较的另一个元素
     * @return -1、0 或 1,分别代表该元素到期时间早于、等于或晚于另一个元素
     */
    @Override
    public int compareTo(Delayed o) {
        if (this.delayTime < ((DelayedElement) o).delayTime) {
            return -1;
        }
        if (this.delayTime > ((DelayedElement) o).delayTime) {
            return 1;
        }
        return 0;
    }

    /**
     * 返回元素的字符串表示
     * @return 元素的字符串表示
     */
    @Override
    public String toString() {
        return "DelayedElement{" +
                "delayTime=" + delayTime +
                ", data='" + data + '\'' +
                '}';
    }
}           

在主函数中,首先创建一个 DelayQueue。然后,我们使用 put 方法将两个 DelayedElement 对象放入队列中。最后,我们一直从队列中取元素并输出它们,直到队列为空。

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayedQueueExample {

    public static void main(String[] args) throws InterruptedException {
        // 创建 DelayQueue
        DelayQueue<DelayedElement> queue = new DelayQueue<>();

        // 将元素放入 DelayQueue,延迟1s
        queue.put(new DelayedElement(1000, "Hello"));
        // 将元素放入 DelayQueue,延迟5s
        queue.put(new DelayedElement(5000, "World"));

        // 获取延迟元素并输出
        while (!queue.isEmpty()) {
            DelayedElement element = queue.take();
            System.out.println(element);
        }
    }
}           

输出结果:

DelayedElement{delayTime=1620478571034, data='Hello'}
DelayedElement{delayTime=1620478576031, data='World'}           

第一个元素的延迟时间为1000毫秒,第二个元素的延迟时间为5000毫秒。

三、使用场景举例

Java 的延迟队列可以用在很多场景中,比如任务调度器。在某个时间点执行某个特定的任务,或者在某个时间段内以指定的频率执行任务。Java 延迟队列可以轻松地实现这样的任务调度器。

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class TaskScheduler {
    private DelayQueue<Task> queue = new DelayQueue<>();

    public void schedule(Task task) {
        queue.offer(task);
    }

    public void run() {
        while (!queue.isEmpty()) {
            try {
                Task task = queue.take();
                task.run();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class Task implements Delayed {
        private Runnable runnable;
        private long executeTime;

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

        public void run() {
            runnable.run();
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return Long.compare(executeTime, ((Task)o).executeTime);
        }
    }
}
           

我们可以在任务调度器中添加一个任务,该任务包含要执行的代码块和延迟时间。当调用 schedule 方法添加任务时,任务会按照其延迟时间插入到延迟队列中。然后,我们可以调用 run 方法来运行任务调度器并等待任务执行。

四、优缺点

优点:

  • 简单易用:Java 的 DelayQueue 工具很容易使用,不需要过多的配置和大量的代码。只需简单实现 Delayed 接口即可。
  • 高效:基于优先级队列的实现方式,DelayQueue 内部使用了堆排序算法,因此可以快速高效地查找、插入和删除元素。即使队列中的元素数量非常庞大,它的性能也不会受到影响。

缺点:

  • 不支持任务取消:一旦任务被添加到 DelayQueue 中,就无法取消或删除它,这可能会导致不必要的资源占用。
  • 对系统内存的消耗:DelayQueue 内部实现是一个优先级队列,随着任务数量的增加,队列大小会逐渐增大,这可能会占用大量的系统内存。
  • 无法保证任务执行的精确时间:由于 DelayQueue 是基于时间的延迟机制,因此任务的执行时间不能保证精确,任务的实际执行时间可能比预期要早或者要晚。

五、总结

延迟队列是一个非常有用的 Java 数据结构,它可以用于多种场景,包括缓存过期、任务超时和心跳检测等。在延迟队列中,每个元素都有一个过期时间,当元素到期时,其相关操作被执行。Java的延迟队列通过维护一个优先级队列来实现,元素以一定的优先级顺序存放在队列中。从延迟队列中获取元素时,如果元素还没有过期,将会被阻塞,直到元素到期或者被删除。在 Java 中,我们可以使用 java.util.concurrent.DelayQueue 来实例化延迟队列。

对于本文的内容,不知道你有没有什么看法,欢迎在评论区里留言。如果你对我的文章内容感兴趣,请点击关注,谢谢支持![谢谢][谢谢][谢谢]

继续阅读