从Timer到Quartz实现动态管理定时任务

本文深入探讨了Java中的定时任务实现,从基于小顶堆的Timer到高效的时间轮算法,再到线程池ScheduledExecutorService,最后详细阐述了Quartz的使用和核心概念。文章通过实例展示了任务调度的原理和常见问题,并提供了并发控制和数据持久化的解决方案。

一:前置知识

在学习定时任务前,需要先了解小顶堆结构,因为Timer和定时任务线程池底层的数据结构都是基于小顶堆,而quartz是基于时间轮算法。

1.1:小顶堆

小顶堆实际上是一个完全二叉树,并且满足:Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]规则,即非叶子结点的值不大于左孩子和右孩子的值。下图就是一个小顶堆:
在这里插入图片描述
完全二叉树很适合用数组做存储,因为它的节点都是紧凑的,且只有最后一层节点数不满:
在这里插入图片描述

而小顶堆在定时任务中的应用,每一个节点就代表一个定时任务,而节点的值就对应着定时任务的到期时间

1.1.1:小顶堆的构建

初始数组为:9,3,7,6,5,1,10,2。按照完全二叉树,将数字依次填入。填入完成后,从最后一个非叶子结点(本示例为数字6的节点)开始调整。根据性质,小的数字往上移动;至此,第1次调整完成。注意,被调整的节点,还有子节点的情况,需要递归进行调整。
在这里插入图片描述
第二次调整,是数字6的节点数组下标小1的节点(比数字6的下标小1的节点是数字7的节点)
在这里插入图片描述
在这里插入图片描述
注意:数字9的节点 将和 数字1的节点 发生对调,对调后,需要递归进行调整
在这里插入图片描述
在这里插入图片描述

1.1.2:小顶堆的插入

以上个小顶堆为例,插入数字0。数字0的节点首先加入到该二叉树最后的一个节点,依据小顶堆的定义,自底向上,递归调整。以下是插入操作的图解:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1.2:小顶堆的删除

对于小顶堆和大顶堆而言,删除是针对于根节点而言。对于删除操作,将二叉树的最后一个节点替换到根节点,然后自顶向下,递归调整。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2:时间轮算法

小顶堆结构其实是有一个问题的,那就是在删除堆顶元素的时候需要把尾部最大元素放到堆顶,然后下沉调整,如果堆很大的话,那么删除操作的性能就会很低

时间轮 是一种实现延迟功能(定时器)的巧妙算法。如果一个系统存在大量的任务调度,时间轮可以高效的利用线程资源来进行批量化调度。把大批量的调度任务全部都绑定时间轮上,通过时间轮进行所有任务的管理,触发以及运行。能够高效地管理各种延时任务,周期任务,通知任务等。

相比于 JDK 自带的 Timer、DelayQueue + ScheduledThreadPool 来说,时间轮算法是一种非常高效的调度模型。不过,时间轮调度器的时间精度可能不是很高,对于精度要求特别高的调度任务可能不太适合,因为时间轮算法的精度取决于时间段“指针”单元的最小粒度大小。比如时间轮的格子是一秒跳一次,那么调度精度小于一秒的任务就无法被时间轮所调度。

时间轮(TimingWheel)算法应用范围非常广泛,各种操作系统的定时任务调度都有用到,我们熟悉的 Linux Crontab,以及 Java 开发过程中常用的 Dubbo、Netty、Akka、Quartz、ZooKeeper 、Kafka 等,几乎所有和 时间任务调度 都采用了时间轮的思想。

时间轮通常有如下三种形式:

  • 链表或数组实现时间轮(while-true-sleep):遍历数组,每个下标放置一个链表,链表节点放置任务,遍历到了就取出执行
  • round型时间轮:任务上记录一个round,遍历到了就将round减一,为0时取出执行,缺点是需要遍历所有的任务,效率较低
  • 分层时间轮:使用多个不同的时间维度的轮,比如天轮是记录几点执行,月轮记录几号执行,月轮遍历到了,就把任务取出放到天轮里面,即可实现几号几点执行

时间轮的具体了解可见这篇博客:时间轮(TimingWheel)高性能定时任务原理解密

二:Timer

在开发过程中,经常性需要一些定时或者周期性的操作。而在Java中则使用Timer对象完成定时计划任务功能。

定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以Timer对象一般又和多线程技术结合紧密。

由于Timer是Java提供的原生Scheduler(任务调度)工具类,不需要导入其他jar包,使用起来方便高效,非常快捷。

下面代码是timer的一个简单示例

public class TimerTest {
   
   
    public static void main(String[] args) {
   
   
        Timer timer = new Timer();  // 任务启动
        for (int i = 0; i < 2; i++) {
   
   
            TimerTask task = new FooTimerTask("foo" + i);
            /*
             * 添加定时任务
             * @param task              添加的具体定时任务
             * @param firstTime         任务第一次执行的时间,new Date()代表立即执行
             * @param period            任务执行间隔,如果间隔为0,则只执行一次
             */
            System.out.println("i :" + i + " 时间" + System.currentTimeMillis());
            timer.schedule(task, new Date(), 2000);
        }
    }
}

class FooTimerTask extends TimerTask {
   
   
    private String name;
    public FooTimerTask(String name) {
   
    this.name = name; }

    @Override
    public void run() {
   
   
        try {
   
   
            System.out.println("name = " + name + " startTime = " + new Date());
            Thread.sleep(3000);  // 任务执行3s
            System.out.println("name = " + name + " endTime = " + new Date());
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
但是执行后,我们可以看到上面的结果,同时也发出疑问,为什么foo0和foo1同时添加进timer,而foo1是添加的3s后才执行。

我们可以翻看Timer类源码:

    /**
     * Creates a new timer.  The associated thread does <i>not</i>
     * {@linkplain Thread#setDaemon run as a daemon}.
     */
    public Timer() {
   
   
        this("Timer-" + serialNumber());
    }

    /**
     * Creates a new timer whose associated thread has the specified name.
     * The associated thread does <i>not</i>
     * {@linkplain Thread#setDaemon run as a daemon}.
     *
     * @param name the name of the associated thread
     * @throws NullPointerException if {@code name} is null
     * @since 1.5
     */
    public Timer(String name) {
   
   
        thread.setName(name);
        thread.start();
    }

可见我们在new Timer()的时候就已经启动了子线程,而thread.start()表示的是以多线程的方式运行

    /**
     * The timer thread.
     */
    private final 
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值