JDK定时器
理论基础
小顶堆
像下面的Timer和定时任务线程池,底层都是小顶堆的结构。
堆是特殊的树,满足下面两个条件就是一个小顶堆:
- 是一颗完全二叉树
- 堆中的某个节点的值总是不大于其父节点的值
插入元素(定时任务):插入尾部,逐步上浮(与父节点比较,进行交换)
删除堆顶元素(执行定时任务):将尾部(最大的元素)放到堆顶,逐步下沉(与子节点比较,进行交换)
时间轮算法
小顶堆只适合相近时间内的小量任务,当执行时间相差过大或任务量很大时,添加新任务或执行堆顶任务时堆化的性能很低。
像Quartz或者其他复杂的定时任务框架,底层更多地会使用时间轮算法。
链表+数组实现
while-true-sleep;遍历数组,每个下标建立一个链表,链表节点中存储任务,遍历到就取出执行。
比如数组的长度为24,每一个数组元素中存储执行任务的链表,相对小顶堆性能有了很大提升,但依然存在问题,比如想在每个月的1号执行任务就不好实现,不够灵活。
round型时间轮
任务上记录一个round值,遍历到便将round值减1,为0时取出执行。
比如数组的长度为24,每个数组元素中存储执行任务的链表,链表节点中除了存储任务,还存储了round值,比如明天的任务就可以设置round为1,当第二遍遍历到时便可取出执行。
存在问题:每次需要遍历所有的任务,效率较低
分层时间轮
使用多个不同时间维度的轮:
- 天轮:记录几点执行
- 月轮:记录几号执行
月轮中匹配当前日期,若存在任务,就到天轮中遍历任务执行,达到几号几点执行任务的需求。
像Linux中定时任务的cron表达式就是典型的分层时间轮
Timer
Timer类中有几个属性需要注意:
1 | //小顶堆,存放timeTask |
特点:
- 单线程执行任务,任务可能相互阻塞
- 运行时异常会导致timer线程终止
- 任务调度时基于绝对时间的,对系统时间敏感
使用实例:
1 | public class TimeTest { |
schedule
与scheduleAtFixedRate
异同:
相同点:
1 | 任务执行未超时,下次执行时间 = 上次执行开始时间 + period; |
区别点:
1 | schedule侧重保持间隔时间的稳定。 |
定时任务线程池
ScheduledThreadPoolExecutor
:
使用多线程执行任务,不会相互阻塞
如果线程失活,会创建新线程执行任务。(线程抛异常,任务会被丢弃,需要做捕获处理)
DalayedWorkQueue:小顶堆,无界队列
在定时线程池中,最大线程数是没有意义的,核心线程数才有意义
执行时间距离当前时间越近的任务在队列的前面
用于添加ScheduleFutureTask(继承于FutureTask,实现RunnableScheduledFuture接口)
- 提供异步执行的能力,并且可以返回执行时间
线程池中的线程从DelayQueue中获取ScheduleFutureTask,然后执行任务
实现了Delayed接口,可以通过getDelay方法获取延迟时间
Leader-Follower模式
- 避免没必要的唤醒和阻塞的操作,节省资源
在Leader-follower线程模型中每个线程有三种模式,leader,follower, processing。
在Leader-follower线程模型一开始会创建一个线程池,并且会选取一个线程作为leader线程,leader线程负责监听网络请求,其它线程为follower处于waiting状态,当leader线程接受到一个请求后,会释放自己作为leader的权利,然后从follower线程中选择一个线程进行激活,然后激活的线程被选择为新的leader线程作为服务监听,然后老的leader则负责处理自己接受到的请求(现在老的leader线程状态变为了processing),处理完成后,状态从processing转换为follower
SingleThreadScheduledExecutor
:
单线程的ScheduledThreadPoolExecutor
1
2
3
4public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
使用示例:
1 | public class ScheduleThreadPoolTest { |
1 | //间隔是固定的,无论上一个任务是否完成 |
定时任务框架-Quartz
官网:Quartz Enterprise Job Scheduler (quartz-scheduler.org)
结构图:
Job:封装为JobDetail设置属性
@DisallowConcurrentExecution
:禁止并发地执行同一个job定义(JobDetail定义的)多个实例@PersistJobDataAfterExecution
:持久化JobDetail中的JobDataMap(对Trigger中的DataMap无效)- 如果一个任务不是持久化的,则当没有触发器关联它时,Quartz会从scheduler中删除它
- 如果一个任务请求恢复,一般是该任务执行期间发生了系统奔溃或者其他关闭进程的操作,当服务再次启动的时候,会再次执行该任务,此时,
JobExercutionContext.isRecovering()
会返回true
Trigger:触发器
优先级
- 同时触发的Trigger之间才会比较优先级
- 如果Trigger是可恢复的,在恢复后再调度时,优先级不变
misfire
:错过触发判断条件:
- job到达触发时间时没有执行
- 被执行的延迟时间超过了Quartz配置的
misfire Threshold
阈值
产生原因:
当job达到触发时间时,所有线程都被其他job占用,没有可用线程
- 再job需要触发的时间点,scheduler停止了
- job使用了
@DisallowConcurrentExecution
注解,job不能并发执行,当达到了下一个job执行点时,上一个任务还未完成 - job指定了过去的开始执行时间,例如当前是8点,指定开始时间为7点
策略:默认都使用
MISFIRE_INSTRUCTION_SMART_POLICY
;Quartz会根据Trigger的类型(SimpleTrigger或CronTrigger)和配置自动选择最合适的处理方式。SimpleTrigger
:具体时间,指定间隔重复执行now*相关的策略,会立即执行第一个misfire的任务,同时会修改
startTime和repeatCount
,导致会重新计算finalFireTime
,打乱原计划next*相关的策略,不会立即执行misfire的任务,补充执行
CronTrigger
:cron表达式MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
:Trigger错过后忽略Misfire策略。例如当前时间是下午2点,而任务本应该在早上10点执行,Quartz框架会等待下一个触发时间,比如下午3点,然后执行任务。它不会考虑错过的10点触发时间,而是仅仅基于当前时间和下一个预定触发时间进行调度。MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
:Trigger错过后立即执行
MISFIRE_INSTRUCTION_DO_NOTHING
:Trigger错过后不做任何处理
calendar
:设置排除时间段
Scheduler:调度器,基于Trigger的设定执行Job
SchedulerFactory:
- 创建Scheduler
- DirectSchedulerFactory:在代码中定制Scheduler
- StdSchedulerFactory:读取classPah下的
quartz.properties
文件来实例化Scheduler
JobStore:存储运行时信息,包括Trigger、Schduler、JobDetail、业务锁等
RAMJobStore(内存实现)
JobStoreTX(JDBC,事务由Quartz管理)
JobStoreCMT(JDBC,使用容器事务)
ClusteredJobStore(集群实现)
TerracottaJobStore(Terracotta中间件)
ThreadPool
- SimpleThreadPool
- 自定义线程池
JobDataMap:保存任务实例的状态信息
- JobDetail:默认旨在Job被添加到调度程序(任务执行计划表)scheduler的时候,存储一次关于该任务的状态信息数据,可以使用注解
@PersistJobDataAfterExecution
注解标明在一个任务执行完毕之后就存储一次 - Trigger:任务被多个触发器引用的时候,根据不同的触发时机,可以提供不同的输入条件
- JobDetail:默认旨在Job被添加到调度程序(任务执行计划表)scheduler的时候,存储一次关于该任务的状态信息数据,可以使用注解
简单使用:
1 |
|
1 | public class quartzTest { |
整合SpringBoot
依赖:
1 | <parent> |
数据库建表脚本:
1 | DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; |
来自官方下载的包中,路径为:quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore
配置文件:
1 | #============================================================================ |
SpringBoot在2.5.6版本之后就删除了关于Quartz相关的依赖;在2.5.6及之前版本:
1 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
SpringBoot只需要配置数据源即可。
配置类:
1 |
|
设置监听器,在SpringBoot启动时启动调度器:
1 |
|
cron表达式最好还是用在线工具生成,手打容易漏空格
1 | public class MyQuartzJob extends QuartzJobBean { |
常用简单实现
使用两个注解便可以基本满足我们定时任务的需求:
- @Scheduled
- @EnableScheduling
实例:
1 |
|
@Scheduled的用法非常灵活,以下是一些使用示例:
1 | 60秒执行一次。 : 每 |