java定时任务
核心算法
小顶堆
堆
-
定义
- 完全二叉树
- 父节点值小于等于子节点值为小顶堆
- 父节点值大于等于子节点值为大顶堆
-
存储
-
链表
-
数组
数组可以很方便的获取下标,由于完全二叉树的特性,将根节点放在下标一位置,其子节点是2n和2n+1;某节点的父节点n/2
-
-
堆元素上浮
- 使用数组方式,每当有新元素加入时,放在队尾,然后和父节点比较,交换值
-
堆元素下沉
- 数组方式,每当删除一个堆顶元素时,将队尾元素放到第一个,然后和子节点比较,交换值
小顶堆算法劣势
小顶堆算法虽然能保证每次都能取到最近的时间,来执行定时任务,但是定时任务一旦多起来,就必须遍历所有的任务。像固定某些天执行的任务,比如每个月的1号执行的任务,其余日期就没必要去和它比较,每周一执行这样的任务,在周二到周日也没必要去参与下沉操作
时间轮算法
普通时间轮
-
描述
每个时间点存一个定时任务列表或数组,比如一天的12小时可以存12个队列,当时间到某一点时,取出对应的队列的任务来执行 -
劣势
假如任务间时间跨度太大,比如有些任务是每天0点0分0秒执行,有些是每年1月1号1点执行,中间还有无数其他任务,那么需要构建的队列的最小粒度是秒,最大粒度是年,一年有31536000秒;如果我希望任务每年1月1号执行一次,最大粒度就不只是年了,时间轮更难实现。
round时间轮
-
描述
每个任务加入一个round值,随后每次到了对应的点便减一,直到为0
-
优势
比较灵活,可以灵活配置各种时间粒度的任务
-
劣势
需要遍历所有任务,尽管可能无需执行
分层时间轮
-
描述
将时间轮分为日轮、月轮、周轮、年轮等,年轮记录哪个月执行,时间到了就丢给月轮,月轮记录几号执行,月轮到了就丢给日轮,日轮记录几点执行,可以丢给时轮…随后再执行,每个时间轮使用小顶堆来存储。cron表达式就是用的这种逻辑。
-
优势
比较灵活,可以灵活配置各种时间粒度的任务,并且没必要遍历一些无需遍历的任务。
java.util.timer
核心类
Timer
-
实现定时任务调度,包含两个类(TaskQueue和TimerThread),包含固定频率调度和按实际成功时间调度
-
核心函数sched(将任务添加到TaskQueue队列)
private void sched(TimerTask task, long time, long period)
- 按实际执行成功时间调度
public void schedule(TimerTask task, Date firstTime, long period)
public void schedule(TimerTask task, long delay, long period)
- 按预设时间调度
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
-
如果是单线程实现定时任务,多个定时任务之间存在阻塞关系,无论采用哪种调度方式,线程都会阻塞,实际调度时间都在上一个任务结束后
class TimeTask extends TimerTask { String name; public TimeTask(String name) { this.name = name; } @Override public void run() { try { System.out.println("name: "+name+";start time"+new Date()); sleep(2000); System.out.println("name: "+name+";end time"+new Date()); } catch (InterruptedException e) { throw new RuntimeException(e); } } } class SingleSchedule { public static void main(String[] args) { Timer timer = new Timer(); for (int i = 0; i < 3; i++) { TimeTask timeTask = new TimeTask("testTask--" + i); // timer.schedule(timeTask, new Date(), 1000); timer.scheduleAtFixedRate(timeTask, new Date(), 1000); } } } name: testTask--0;start timeTue Apr 25 20:11:52 CST 2023 name: testTask--0;end timeTue Apr 25 20:11:54 CST 2023 name: testTask--1;start timeTue Apr 25 20:11:54 CST 2023 name: testTask--1;end timeTue Apr 25 20:11:56 CST 2023 name: testTask--2;start timeTue Apr 25 20:11:56 CST 2023 name: testTask--2;end timeTue Apr 25 20:11:58 CST 2023 name: testTask--0;start timeTue Apr 25 20:11:58 CST 2023 name: testTask--0;end timeTue Apr 25 20:12:00 CST 2023 name: testTask--1;start timeTue Apr 25 20:12:00 CST 2023 name: testTask--1;end timeTue Apr 25 20:12:02 CST 2023 name: testTask--2;start timeTue Apr 25 20:12:02 CST 2023 name: testTask--2;end timeTue Apr 25 20:12:04 CST 2023 name: testTask--1;start timeTue Apr 25 20:12:04 CST 2023
TaskQueue
- Timer文件中的类,存储TimerTask任务队列,小顶堆数组
- 队列初始大小128,自动扩容大小是原来的双倍
TimerThread
-
Timer文件中的类,继承了Thread类,用于多线程执行定时任务并设置下次执行时间
-
核心方法mainLoop核心逻辑:
-
任务的删除、下沉
-
设置下次执行时间
queue.rescheduleMin( task.period<0 ? currentTime - task.period //实际执行成功时间调度 : executionTime + task.period);//按预设时间调度
-
TimerTask
抽象类实现Runnable接口,使用定时任务时需要新建一个类继承此类并实现run方法
主要算法
小顶堆(数组存储)
-
初始化:大小128的定时任务数组
-
扩容:每次扩容大小为之前的两倍
-
上浮:新加入任务时将其加入到队尾,并不停与父节点比较,交换位置,直到符合小顶堆规则
-
下沉:删除一个任务时,将其队尾任务替换,并循环与其子节点比较,交换位置,直到符合小顶堆规则
-
更新:更新任务时,设置好其下次执行时间,然后下沉
使用线程池ScheduledThreadPoolExecutor
Leader-Follow模式
只有leader线程执行任务,其他为follower线程,leader线程执行完后,变为follower线程
避免没必要的唤醒和阻塞操作,高效且节约资源
ScheduledFutureTask
继承于FutureTask
DelayedWorkQueue
小顶堆,无界队列
改进
不会被别的任务阻塞,阻塞只会发生在自身任务
class TimeTask extends TimerTask {
String name;
public TimeTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println("name: "+name+";start time"+new Date());
sleep(2000);
System.out.println("name: "+name+";end time"+new Date());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
class SingleSchedule {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
for (int i = 0; i < 3; i++) {
TimeTask timeTask = new TimeTask("testTask--" + i);
// scheduledThreadPool.schedule(timeTask,1, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(timeTask,0,1,TimeUnit.SECONDS);
}
}
}
name: testTask--2;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--2;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--2;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--2;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--2;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:27 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:27 CST 2023
Quartz框架
简介
成熟的java定时任务框架
核心元素
JobDetail
Trigger
Scheduler
Listener
Cron表达式
quartz使用了时间轮和小顶堆算法,在CronExpression类中年月日时分秒各有自己的TreeSet
private final String cronExpression;
private TimeZone timeZone = null;
protected transient TreeSet<Integer> seconds;
protected transient TreeSet<Integer> minutes;
protected transient TreeSet<Integer> hours;
protected transient TreeSet<Integer> daysOfMonth;
protected transient TreeSet<Integer> months;
protected transient TreeSet<Integer> daysOfWeek;
protected transient TreeSet<Integer> years;
字段 允许值 允许的特殊字符
秒 0-59 , - * /
分 0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
年(可选) 留空, 1970-2099 , - * /
表达式 意义
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
特殊字符 意义
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
L("last") ("last") "L" 用在day-of-month字段意思是 "这个月最后一天";用在 day-of-week字段, 它简单意思是 "7" or "SAT"。 如果在day-of-week字段里和数字联合使用,它的意思就是 "这个月的最后一个星期几" – 例如: "6L" means "这个月的最后一个星期五". 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。
W("weekday") 只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。
# 只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
C 指和calendar联系后计算过的值。例:在day-of-month 字段用“5C”指在这个月第5天或之后包括calendar的第一天;在day-of-week字段用“1C”指在这周日或之后包括calendar的第一天
spring整合quartz
在springboot中,ScheduleFactoryBean可以自动注入一个默认的,并且可以隐式的调用Scheduler,从而使得不用显式去结合Job和Trigger。参考链接:
https://www.jianshu.com/p/52bf3f3aab6c
https://medium.com/@manvendrapsingh/quartz-scheduling-in-springboot-7cea1b7b19e7
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html#setDataSource(javax.sql.DataSource)
配置特性文件
-
properties
server.port=8000 spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=cc1234 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.platform=mysql spring.datasource.schema=classpath:tables_mysql.sql #quartz spring.quartz.auto-startup=true spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_ spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=10000 spring.quartz.properties.org.quartz.jobStore.useProperties=false spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool spring.quartz.properties.org.quartz.threadPool.threadCount=10 spring.quartz.properties.org.quartz.threadPool.threadPriority=5 spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true cron.test=*/5 * * * * ?
-
yml
server:
port: 8862
servlet:
context-path: /quartzService
spring:
application:
name: quartzService
datasource:
quartz:
#如果需要quartz 第一次运行时自动生成 quartz 所需的表那么后面的配置必须有:allowMultiQueries=true
#待第一次运行后可以再根据自己的需要修改
url: jdbc:mysql://localhost:3306/quartzJob?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
quartz:
#相关属性配置
properties:
org:
quartz:
scheduler:
#调度器实例名称
instanceName: clusteredScheduler
#调度器实例编号自动生成
instanceId: AUTO
jobStore:
#持久化方式配置
class: org.quartz.impl.jdbcjobstore.JobStoreTX
#持久化方式配置数据驱动,MySQL数据库
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#quartz相关数据表前缀名
tablePrefix: QRTZ_
#开启分布式部署
isClustered: true
#分布式节点有效性检查时间间隔,单位:毫秒
clusterCheckinInterval: 10000
#配置是否使用
useProperties: false
threadPool:
#线程池实现类
class: org.quartz.simpl.SimpleThreadPool
#执行最大并发线程数量
threadCount: 10
#线程优先级
threadPriority: 5
#配置是否启动自动加载数据库内的定时任务,默认true
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: jdbc
#初始化表结构
jdbc:
initialize-schema: always #always 属性意思是,每次初始化都会重新生成表(执行一次删除,执行一次创建),生成后,可以修改为 never
任务配置文件
@Configuration
public class QuartzConfig {
@Value("${cron.test}")
private String testCron;
@Bean
public JobDetail testJobDetail() {
return JobBuilder.newJob()
.ofType(TaskTestJob.class)
.withIdentity("test1")
.storeDurably()
.build();
}
@Bean
public Trigger testTrigger() {
return TriggerBuilder.newTrigger()
.withIdentity("test1")
.forJob(testJobDetail())
.withSchedule(CronScheduleBuilder.cronSchedule(testCron))
.build();
}
}
任务类
public class TaskTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("now time:" + new Date());
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
执行结果分析
任务是多线程、并且是固定时间触发的
now time:Thu Apr 27 19:35:50 CST 2023
now time:Thu Apr 27 19:35:55 CST 2023
now time:Thu Apr 27 19:36:00 CST 2023
now time:Thu Apr 27 19:36:05 CST 2023
now time:Thu Apr 27 19:36:10 CST 2023
now time:Thu Apr 27 19:36:15 CST 2023
now time:Thu Apr 27 19:36:20 CST 2023
now time:Thu Apr 27 19:36:25 CST 2023
now time:Thu Apr 27 19:36:30 CST 2023
now time:Thu Apr 27 19:36:35 CST 2023
now time:Thu Apr 27 19:36:40 CST 2023
now time:Thu Apr 27 19:36:45 CST 2023
now time:Thu Apr 27 19:36:50 CST 2023
now time:Thu Apr 27 19:36:55 CST 2023