java定时任务目前主要有三种:
- Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask,一般用的较少。
- Spring3.0以后自带的task,即:spring schedule,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
- Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;代码稍显复杂。
定时器算法
1.小顶堆
堆,实际上是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。
堆又分为两种,最大堆、最小堆。
- 最大堆: 任一非叶子节点的值均大于其左子节点和右子节点的值。根节点的值是最大的。
- 最小堆: 任一非叶子节点的值均小于其左子节点和右子节点的值。根节点的值是最小的。
小顶堆的实现方式
由于堆是一种经过排序的完全二叉树,因此在构建的时候需要对其新插入的节点进行一些操作以使其符合堆的性质。这种操作就叫上浮与下沉。
- 上浮:将当前节点与其父节点相比,如果当前节点的值比父节点小,就把当前节点与父节点交换,然后继续前面的交换,直到当前节点比父节点的值大为止。上浮就是将符合条件的节点往上移的过程。
- 下沉:将当前节点与其左、右子节点相比,如果当前节点的值比其中一个或两个子节点的值大,就把当前节点与两个子节点种比较小的那个交换,,然后继续前面的比较,直到当前节点的值比两个子节点的值都小为止。下沉就是将符合条件的节点往下移的过程。
2.时间轮算法
见名知意,时间轮算法的数据结构类似于钟表上的数据指针,时间轮用环形数组的方式实现,数组中的每个元素都可以称之为槽(和redis集群的槽一样称呼)。槽的内部用双向链表存储着待执行的任务,添加和删除链表的操作时间复杂度为O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是1秒。
当有一个延迟任务要插入时间轮时,首先计算其延迟时间与单位时间的余值,从指针指向的当前槽位移动余值的个数槽位,就是该延迟任务需要被放入的槽位。
举个例子,时间轮有8个槽位,编号为 0 ~ 7 。指针当前指向槽位 2 。新增一个延迟时间为 4 秒的延迟任务,4 % 8 = 4,因此该任务会被插入 4 + 2 = 6,也就是槽位6的延迟任务队列。
时间槽位的实现方式
时间轮的槽位实现可以采用循环数组的方式达成,也就是让指针在越过数组的边界后重新回到起始下标。概括来说,可以将时间轮的算法描述为:
用队列来存储延迟任务,同一个队列中的任务,其延迟时间相同。用循环数组的方式来存储元素,数组中的每一个元素都指向一个延迟任务队列。有一个当前指针指向数组中的某一个槽位,每间隔一个单位时间,指针就移动到下一个槽位。被指针指向的槽位的延迟队列,其中的延迟任务全部被触发。在时间轮中新增一个延迟任务,将其延迟时间除以单位时间得到的余值,从当前指针开始,移动余值对应个数的槽位,就是延迟任务被放入的槽位。
基于这样的数据结构,插入一个延迟任务的时间复杂度就下降到 O(1) 。而当指针指向到一个槽位时,该槽位连接的延迟任务队列中的延迟任务全部被触发。
延迟任务的触发和执行不应该影响指针向后移动的时间精确性。因此一般情况下,用于移动指针的线程只负责任务的触发,任务的执行交由其他的线程来完成。比如,可以将槽位上的延迟任务队列放入到额外的线程池中执行,然后在槽位上新建一个空白的新的延迟任务队列用于后续任务的添加。
代码实现
Timer
/**
* @className: TimerTest
* @description: 测试java.util.Timer的定时器实现
* @author: charon
* @create: 2021-10-10 10:35
*/
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
// 延迟1s执行任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟1s执行的任务"+new Date());
}
},1000);
// 延迟3s执行任务,每隔5s执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟3s每隔5s执行一次的任务"+new Date());
}
},3000,5000);
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// timer.cancel();
// System.out.println("任务执行完毕"+new Date());
}
}
延迟1s执行的任务Sun Oct 10 14:34:13 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:15 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:20 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:25 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:30 CST 2021
Timer的实现方式比较简单,其内部有两个主要的属性:
/**
* 用于存放定时任务TimeTask的列表
*/
private final TaskQueue queue = new TaskQueue();
/**
* 用于执行定时任务的线程
*/
private final TimerThread thread = new TimerThread(queue);
TimerTask是一个实现了Runnable接口的抽象类。其run()方法用于提供具体的延时任务逻辑。
TaskQueue内部采用的是小顶堆的算法实现。根据任务的触发时间采用死循环的方式进行排序,将执行时间最小的任务放在前面。
void add(TimerTask task) {
// Grow backing store if necessary
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);
}
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k