定时任务

定时器是操作系统的重要组成部分,它可以周期性的发出信号或中断,以便操作系统或其他应用程序可以在指定时间间隔内执行某些任务。在定时任务中,操作系统或应用程序会利用计时器或定时器定期检查当前时间是否为预定的执行时间,如果达到预定时间则执行任务。主流的操作系统提供了Crontab、Windows Task Scheduler 等管理工具。

Cron表达式

Cron 表达式是一种用于调度任务的时间表达式,在配置各种定时任务中很常见。它由6或7个字段组成,字段之间用空格分隔,分别表示秒、分、时、日、月、周几、年(可选)。每个字段都有特定的取值范围和支持的特殊表达字符。

  • * 用于代表所以可能的值
  • / 用于指定该字段的间隔值
  • ? 用于日、周、年不指定值

举例:
* * * * * * 表示每时每刻都在执行
0 0 12 ? * WED 表示在每星期三中午12点执行
0 0/30 9-17 * * ? 表示在朝九晚五的每半小时执行一次
0 * * * * * 2024 表示2024年内的每分钟执行一次

Java定时任务

JDK中提供了几个类可以用于实现定时任务,可简单的用于实现定时任务,不过这些类都是基于JVM内存的,需要把任务存储到内存中,如果数据量过大可能导致OOM,如果应用重启任务信息会丢失。

  • Timer、TimerTask

    Timer 是一个定时器工具类,可以用于执行定时任务,TimerTask 类是一个可调度的任务,通过继承该类实现任务内容,然后使用 Timer.schedule() 方法安排任务的执行时间。

    Timer 内部有两个重要的成员变量 TaskQueueTimerThread 。一个队列用于存储任务并按执行时间排序;另一个(单)线程用于扫描队列中的任务,到达执行时间后执行任务的 run() 方法,这个线程是一个守护线程,其他非守护线程完成时不管任务是否完成都会终止。

  • ScheduledExecutorService

    ScheduledExecutorService 是一个定时任务执行器,通过多线程支持多个任务并发执行,调用 ScheduledExecutorService.scheduler()ScheduledExecutorService.scheduleAtFixedRate() 来安排任务的执行时间。

  • DelayQueue

    DelayQueue 是一个带有延迟时间的无界阻塞队列,它的元素必须实现 Delayed 接口,从队列取出元素时,如果延迟时间还未到达,则会阻塞队列,直到满足设置的延迟时间。可以将任务信息封装为实现 Delayed 的接口然后放入延迟队列中,再额外使用一个线程不断地从延迟队列中取出元素执行任务,从而实现定时任务的调度。

实现定时任务的数据结构与算法

  • 小顶堆

    小顶堆是一种特殊的二叉树堆数据结构,其上级节点的值小于等于子节点的值,最小值总数位于堆的根节点上。使用小顶堆管理定时事件,每个事件中包含触发事件戳和其他任务信息,时间戳最近(小)的任务一直处于堆的顶部,可以高效的找到最近将要触发的定时任务。

  • 时间轮算法

    时间轮算法是一种时间管理算法,可以高效的处理定时任务。算法将时间划分为若干个时间槽,每个槽表示一个时间段,这个时间段可以根据实际需要以秒级、分钟级、小时级等等划分(可以用时钟去理解)。有定时任务需要执行时,就把任务放入时间槽中。这个任务将会在对应时间的槽位上触发(例如:将一个时间轮划分为60个槽位对应60秒,当前时间为0秒,如果要在3秒后执行,就放入第3个槽位)。时间轮算法使用循环队列来存储在每个时间槽上触发的任务,避免反复遍历整个定时任务集合的开销。

    如果时间轮的周期较长,需要的槽位较多时,可以向时间轮加入轮数(round)标识,当随着时间移动到某个槽位时,检查轮数是否符合要求,不符合就继续向前不触发,但这仍需要遍历所有任务,效率仍然不够高。可以使用分层的时间轮,在秒级的基础上加入其他级别的时间轮,每个级别的时间轮都独立旋转,当一个级别时间轮满足条件后,就升级到下一级时间轮,升级后任务调度的颗粒度变得更细,意味着将会在更短的时间内被触发。

  • 链表

    链表用于串联管理定时任务,包含触发时间戳和任务信息,通过遍历链表可以找到最近触发的定时任务。

第三方Java定时任务库或框架

  • Spring 的 @Scheduled 注解

    Spring 提供了一个方便的定时任务调度,只需要在启动类上加上 @EnableScheduling 注解启用 Spring 框架的定时任务调度功能,然后在需要定时执行的方法加上 @Scheduled 注解填入执行任务的 Cron 表达式即可。

    @Scheduled 适用于单体应用,分布式多实例的情况下如果处理不当会有重复多次执行的问题。另外,注解实现的任务不支持并发执行,如果到了下一次任务的开始时间,前一次任务还没有执行完成,下一次任务调度会被丢弃。

  • Quartz、xxl-job、Elastic-Job

    它们是开源的分布式任务调度框架或平台,支持任务的并发调度执行。