目录
- 一、定时器时间配置规则
- 1.常用示例
- 二、java三种定时任务:
- 1.定时任务代码实现
- 1.1 Timer
- 问题
- 1.2 Schedule
- 1.3 Quartz
一、定时器时间配置规则
格式: [秒] [分] [小时] [日] [月] [周] [年]
序号 | 说明 | 是否必填 | 允许填写的值 | 允许的通配符 |
1 | 秒 | 是 | 0-59 | , - * / |
2 | 分 | 是 | 0-59 | , - * / |
3 | 小时 | 是 | 0-23 | , - * / |
4 | 日 | 是 | 1-31 | , - * ? / L W |
5 | 月 | 是 | 1-12 or JAN-DEC | , - * / |
6 | 周 | 是 | 1-7 or SUN-SAT | , - * ? / L # |
7 | 年 | 否 | empty 或 1970-2099 | , - * / |
通配符说明:
- * 表示所有值. 例如:在分的字段上设置 “*”,表示每一分钟都会触发。
- ? 表示不指定值使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- - 表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发。
- , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
- / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次。
- L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
- W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W"前只能设置具体的数字,不允许区间”-").
1.常用示例
表达式 | 含义 |
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分每分触发 |
0 0/5 14 * * ? | 每天下午的 2点到2点59分(整点开始,每隔5分触发) |
0 0/5 14,18 * * ? | 每天下午的 2点到2点59分(整点开始,每隔5分触发) 每天下午的 18点到18点59分(整点开始,每隔5分触发) |
0 0-5 14 * * ? | 每天下午的 2点到2点05分每分触发 |
0 10,44 14 ? 3 WED | 3月分每周三下午的 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 | 每月的第三周的星期五开始触发 |
0 0 12 1/5 * ? | 每月的第一个中午开始每隔5天触发一次 |
0 11 11 11 11 ? | 每年的11月11号 11点11分触发(光棍节) |
二、java三种定时任务:
- Java自带的
java.util.Timer类
,这个类允许你调度一个java.util.TimerTask
任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask
,一般用的较少。 - Spring3.0以后自带的task,即:
spring schedule
,可以将它看成一个轻量级的Quartz
,而且使用起来比Quartz简单许多。 -
Quartz
,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;代码稍显复杂。
1.定时任务代码实现
1.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 = j;
}
}
问题
这样的方式就会有三个问题:
- 由于
执行任务的线程只有一个
,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。 - 如果执行某个任务过程中
抛出了异常,那么执行线程将会终止
,导致Timer中的其他任务也不能再执行。 -
Timer使用的是绝对时间
,即是某个时间点,所以它执行依赖系统的时间
,如果系统时间修改了的话,将导致任务可能不会被执行。
由于Timer存在上面说的这些缺陷,在JDK1.5中,我们可以使用ScheduledThreadPoolExecutor
来代替它,使用Executors.newScheduledThreadPool工厂方法
或使用ScheduledThreadPoolExecutor的构造函数
来创建定时任务,它是基于线程池的实现,不会存在Timer存在的上述问题,当线程数量为1时,它相当于Timer。
1.2 Schedule
<!--Spring Schedule在使用前都需要引入spring的包。-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
在这里我主要是使用spring boot注解的方式来实现:
/**
* 在spring boot的启动类上面添加 @EnableScheduling 注解
*/
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class,args);
}
}
新创建一个类,用来实现定时任务,这个类要注册成为Bean才行。
/**
* @className: ScheduleTest
* @description: 测试schedule的执行器
* @author: charon
* @create: 2021-10-10 19:04
*/
@Component
public class ScheduleTest {
/**
* corn表达式:秒、分、时、日、月、星期
* 值可以是数字,也可以是以下符号:
* *:所有值都匹配 示例: 0 0 * * * *:每小时(当秒和分都为0的时候)
* ?:只能用在日期和星期两个表达式中 示例: 0 0 12 * * ? 每天中午12点触发
* ,:或者 示例:0 0 9,13 * * *:每天的9点和13点
* /:增量值 示例: * /10 * * * * *:每10秒
* -:区间 示例: 0 0/30 9-17 * * ? : 朝九晚五工作时间内每半小时
*/
@Scheduled(cron="0 * * * * *")
public void doSomething(){
System.out.println("测试schedule的定时器,当秒为0的时候执行一次:"+new Date());
}
}
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:18:00 CST 2021
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:19:00 CST 2021
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:20:00 CST 2021
@Scheduled注解的另外两个重要属性:fixedRate
和fixedDelay
fixedDelay:上一个任务结束后多久执行下一个任务
fixedRate:上一个任务的开始到下一个任务开始时间的间隔
/**
* 测试fixedRate,每2s执行一次
* @throws Exception
*/
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {
System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));
//休眠1秒
Thread.sleep(1000);
System.out.println("fixedRate执行结束时间:" + new Date(System.currentTimeMillis()));
}
fixedRate开始执行时间:Sun Oct 10 19:59:05 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:06 CST 2021
fixedRate开始执行时间:Sun Oct 10 19:59:07 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:08 CST 2021
fixedRate开始执行时间:Sun Oct 10 19:59:09 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:10 CST 2021
/**
* 等上一次执行完等待1s执行
* @throws Exception
*/
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws Exception {
System.out.println("fixedDelay开始执行时间:" + new Date(System.currentTimeMillis()));
//休眠两秒
Thread.sleep(1000 * 2);
System.out.println("fixedDelay执行结束时间:" + new Date(System.currentTimeMillis()));
}
fixedDelay执行结束时间:Sun Oct 10 20:07:23 CST 2021
fixedDelay开始执行时间:Sun Oct 10 20:07:24 CST 2021
fixedDelay执行结束时间:Sun Oct 10 20:07:26 CST 2021
fixedDelay开始执行时间:Sun Oct 10 20:07:27 CST 2021
fixedDelay执行结束时间:Sun Oct 10 20:07:29 CST 2021
如果是强调任务间隔的定时任务,建议使用fixedRate
和fixedDelay
,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式
。
Spring Schedule的Corn是使用的时间轮算法(分层时间轮,每个时间粒度对应一个时间轮,多个时间轮时间进行级联协调)。在CronSequenceGenerator.java这个类中,对每个CornTask都维护了一下7个Bitset
(使用位数组而不用list,set之类的数据结构,一方面是因为空间效率,更重要的是接下来的操作主要是判断某个值是否匹配和从某个值开始找最近的下一个能够匹配的值)
private final BitSet months = new BitSet(12);
private final BitSet daysOfMonth = new BitSet(31);
private final BitSet daysOfWeek = new BitSet(7);
private final BitSet hours = new BitSet(24);
private final BitSet minutes = new BitSet(60);
private final BitSet seconds = new BitSet(60);
然后根据配置的corn值计算这个任务对应的值计算每个bit的值。如我这里配置的每分钟执行一次的CornTask的结果如下:
CronSequenceGenerator负责解析用户配置的Cron表达式
,并提供next
方法,根据给定的时间获取符合cron表达式规则的最近的下一个时间。CronTrigger实现Trigger的nextExecutionTime方法
,根据定时任务执行的上下文环境(最近调度时间和最近完成时间)决定查找下一次执行时间的左边界,之后调用CronSequenceGenerator的next方法从左边界开始找下一次的执行时间。
CronSequenceGenerator的doNext算法从指定时间开始(包括指定时间)查找符合cron表达式规则下一个匹配的时间。如图3-4所示,其整体思路是:沿着秒→分→时→日→月逐步检查指定时间的值
。如果所有域上的值都已经符合规则那么指定时间符合cron表达式,算法结束。否则,必然有某个域的值不符合规则,调整该域到下一个符合规则的值(可能调整更高的域),并将较低域的值调整到最小值,然后从秒开始重新检查和调整。
private void doNext(Calendar calendar, int dot) {
List<Integer> resets = new ArrayList<>();
int second = calendar.get(Calendar.SECOND);
List<Integer> emptyList = Collections.emptyList();
int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList);
if (second == updateSecond) {
resets.add(Calendar.SECOND);
}
int minute = calendar.get(Calendar.MINUTE);
int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets);
if (minute == updateMinute) {
resets.add(Calendar.MINUTE);
}
else {
doNext(calendar, dot);
}
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets);
if (hour == updateHour) {
resets.add(Calendar.HOUR_OF_DAY);
}
else {
doNext(calendar, dot);
}
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, this.daysOfWeek, dayOfWeek, resets);
if (dayOfMonth == updateDayOfMonth) {
resets.add(Calendar.DAY_OF_MONTH);
}
else {
doNext(calendar, dot);
}
int month = calendar.get(Calendar.MONTH);
int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets);
if (month != updateMonth) {
if (calendar.get(Calendar.YEAR) - dot > 4) {
throw new IllegalArgumentException("Invalid cron expression \"" + this.expression +
"\" led to runaway search for next trigger");
}
doNext(calendar, dot);
}
}
private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List<Integer> lowerOrders) {
int nextValue = bits.nextSetBit(value);
//下一个匹配值是-1,则将对更高的域做加1操作,从0开始查找下一个匹配值,将当前域设置为下一个匹配值,重置比当前域低的所有域设置为最小值,递归调度本算法。
if (nextValue == -1) {
calendar.add(nextField, 1);
reset(calendar, Collections.singletonList(field));
nextValue = bits.nextSetBit(0);
}
//下一个匹配值不是当前值但也不是-1,则将当前域设置为下一个匹配值,将比当前域低的所有域设置为最小值,递归调度本算法
if (nextValue != value) {
calendar.set(field, nextValue);
reset(calendar, lowerOrders);
}
// 下一个匹配值就是当前值,则匹配通过,如果当前域是月则算法结束,否则继续处理下一个更高的域。
return nextValue;
}
1.3 Quartz
在这里还是使用Spring Boot 集成Quartz;
引入依赖:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
测试的job业务处理类:
/**
* @className: QuartzJob
* @description: 业务逻辑处理类
* @author: charon
* @create: 2021-10-11 14:32
*/
public class QuartzJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("执行quartz定时器开始:" + new Date());
// 模拟业务逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行quartz定时器结束:" + new Date());
}
}
实例化Job,将任务触发器加入任务调度中:
/**
* @className: QuartzConfig
* @description: scheduler的启动、结束等控制类
* @author: charon
* @create: 2021-10-11 14:38
*/
@Configuration
public class QuartzConfig {
@Autowired
private Scheduler scheduler;
/**
* 开始定时器
*/
public void startJob() throws SchedulerException {
// 通过JobBuilder构建JobDetail实例,JobDetail规定只能是实现Job接口的实例
// JobDetail 是具体Job实例
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job", "group").build();
// 基于表达式构建触发器 每5秒种执行一次
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
// CronTrigger表达式触发器 继承于Trigger
// TriggerBuilder 用于构建触发器实例
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
.withSchedule(cronScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, cronTrigger);
}
/**
* 删除某个任务
*
* @param name job的名称
* @param group job的分组
* @throws SchedulerException
*/
public void deleteJob(String name, String group) throws SchedulerException {
JobKey jobKey = new JobKey(name, group);
if (scheduler.checkExists(jobKey)){
scheduler.deleteJob(jobKey);
}
}
}
测试类(spring容器初始化完成后执行):
/**
* @className: QuartzTest
* @description: 测试quartz的定时器
* @author: charon
* @create: 2021-10-11 10:33
*/
@Configuration
public class QuartzTest implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private QuartzConfig quartzConfig;
/**
* 监听初始化quartz
* @param event
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("容器初始化完成");
try {
quartzConfig.startJob();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
容器初始化完成
2021-10-11 15:02:46.947 INFO 19628 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 7010 (http) with context path ''
2021-10-11 15:02:49.558 INFO 19628 --- [ main] c.c.ScheduleApplication : Started ScheduleApplication in 10.734 seconds (JVM running for 11.644)
执行quartz定时器开始:Mon Oct 11 15:02:50 CST 2021
执行quartz定时器结束:Mon Oct 11 15:02:52 CST 2021
执行quartz定时器开始:Mon Oct 11 15:02:55 CST 2021
执行quartz定时器结束:Mon Oct 11 15:02:57 CST 2021
执行quartz定时器开始:Mon Oct 11 15:03:00 CST 2021
执行quartz定时器结束:Mon Oct 11 15:03:02 CST 2021