Bootstrap

Java定时任务实现方案(三)——DelayQueue(JUC包)

DelayQueue(JUC包)

这篇笔记,我们要来介绍实现Java定时任务的第三个方案,使用DelayQueue,以及该方案的优点和缺点。

​ DelayQueue是Java并发包java.util.concurrent中的一个无界阻塞队列,它只允许插入实现了Delay接口的对象,队列中的元素只有当其延迟时间到达之后才能被取走,我们这里就是基于DelayQueue的阻塞特性、延迟特性和无界性来实现的定时任务调度。

​ 阻塞特性指DelayQueue的take方法会一直阻塞,直到队列中有元素的延迟时间到期,然后返回该元素。

​ 延迟特性指每一个插入到DelayQueue的元素都有一个关联的延迟时间,只有当延迟时间到期后,该元素才能从队列中取出。

​ 无界性指DelayQueue是无界的,可以无限添加元素,但只有延迟到期的元素才能被取出。

​ 定时任务的大致工作流程是创建任务->任务入延迟队列->消费者线程在任务到期时取出任务->执行任务->重复上述流程。

使用
1.创建一个定时任务类

​ DelayQueue延迟队列只允许插入实现了Delay接口的对象,所以我们的定时任务是用一个实现了Delay接口的类来表示的。实现Delay接口必须实现两个方法,其中getDelay方法用来返回任务的剩余延迟时间,compareTo方法用来比较当前定时任务和其他定时任务的执行的先后顺序。

​ 我们定时任务的代码逻辑通过实现Runable接口的一个类经过构造器传入到我们的定时任务类当中。

public class DelayedTask implements Delayed {
    private final long executionTime;
    private final Runnable task;
    public DelayedTask(long delay,TimeUnit unit,Runnable task){
        this.executionTime = System.currentTimeMillis() + unit.toMillis(delay);
        this.task = task;
    }

    /**
     * 返回任务的剩余延迟时间
     * @param unit the time unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        long diff = executionTime - System.currentTimeMillis();
        return unit.convert(diff,TimeUnit.MILLISECONDS);
    }

    /**
     * 比较两个任务的执行时间,以便延迟队列能够正确地按顺序处理任务
     * @param o the object to be compared.
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.executionTime,((DelayedTask) o).executionTime);
    }
    public void run(){
        task.run();
    }
}
2.创建一个定时任务消费者类

​ 接着,我们需要创建一个定时任务消费者类,这个类的实例将从延迟队列中取出到达指定时间的定时任务,执行相应的代码逻辑。DelayQueue的take方法会阻塞地获取任务,所以我们最好实现一个Runnable接口,通过开辟主线程之外的线程来消费定时任务。

public class TaskConsumer implements Runnable{
    private final DelayQueue<DelayedTask> queue;
    public TaskConsumer(DelayQueue<DelayedTask> queue){
        this.queue = queue;
    }
    @Override
    public void run() {
        while(true){
            try {
                //阻塞地获取任务,从延迟队列中取出任务并执行
                DelayedTask task = queue.take();
                task.run();
            } catch (InterruptedException e) {
                //中断异常,结束线程
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}
3.创建一个定时任务生产者类

​ 然后,我们还需要一个将定时任务添加到延迟队列中的生产者,这里我们也在主线程之外进行添加,这一步可以根据具体的需要进行删除或保留,在实际需求中,我们可能在主线程中将定时任务添加到延迟队列即可。

public class TaskProducer implements Runnable{
    private final DelayQueue<DelayedTask> queue;
    public TaskProducer(DelayQueue<DelayedTask> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        //添加一些任务
        queue.put(new DelayedTask(5, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务1执行")));
        queue.put(new DelayedTask(10, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务2执行")));
        queue.put(new DelayedTask(15, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务3执行")));
    }
    /**
     * 获取当前系统时间
     * @return
     */
    public static String getTime(){
        //获取当前的系统时间
        LocalDateTime now = LocalDateTime.now();
        //定义时间格式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        //格式化时间
        return now.format(formatter);
    }
}
4.启动生产者和消费者进程,实现定时任务

​ 最后,同时启动生产者和消费者进程,就可以添加和执行定时任务了。

public class DelayQueueExample {
    public static void main(String[] args) {
        DelayQueue<DelayedTask> queue = new DelayQueue<>();

        Thread consumerThread = new Thread(new TaskConsumer(queue));
        Thread producerThread = new Thread(new TaskProducer(queue));

        consumerThread.start();
        producerThread.start();
    }
}
优点
1.简单易用

​ DelayQueue是Java自带的并发工具类,不需要额外引入依赖。通过简单的生产者-消费者模式即可实现定时任务调度。

2.高精度延迟控制

​ DelayQueue提供了精确的延迟控制功能,只有当任务的延迟时间到达后,任务才会从队列中取出并执行,适用于需要高精度延迟的任务场景。

3.线程安全

​ DelayQueue是线程安全的,多个线程可以同时对其进行操作而不会出现数据不一致的问题,适合多线程环境下的任务调度。

4.阻塞特性

​ DelayQueue的take方法会阻塞到有任务到期为止,减少了不必要的轮询和资源浪费,提高了系统的效率。

5.灵活性

​ 可以根据实际需要自定义Delayed接口的实现类,灵活控制任务的延迟时间和执行逻辑。

6.使用于一次性任务

​ 对于只需要执行一次的任务,DelayQueue是个不错的选择,它能够高效地管理这些一次性的任务,并在指定的时间点触发执行。

缺点
1.不支持周期性任务

​ DelayQueue只能处理一次性延迟任务,无法直接支持周期性任务。如果需要周期性执行的任务,必须手动重新提交任务到队列中,增加了复杂性和出错的可能性。

2.依赖外部线程管理

​ 需要自己管理生产者和消费之线程,这不仅增加了代码的复杂性,还可能导致资源管理不当,出现线程泄漏或线程阻塞问题。

3.性能开销较大

​ 每次从DelayQueue中取出元素时,都会进行一次排序操作以确保延迟最小的元素优先出队,对于高并发或大量任务场景,这种排序操作可能会带来较大的性能开销。

4.缺乏高级调度功能

​ DelayQueue提供的功能相对简单,缺少一些高级调度特性,如任务优先级、任务依赖关系、任务重试机制等,这些高级调度特性在复杂的业务场景中往往是必需的。

5.异常处理复杂

​ 如果任务执行过程中发生异常,DelayQueue本身是不会自动处理这些异常的,需要我们自己在消费者线程中添加额外的异常处理逻辑,以确保系统的稳定性和可靠性。

6.不适合长时间运行任务

​ 如果任务的执行时间超过了其设定的延迟时间,可能会导致后续任务的延迟执行,进而影响整个调度系统的准确性。

;