一:前置知识
在学习定时任务前,需要先了解小顶堆结构,因为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

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





