目录
一、定时任务的理解
二、入门案例
三、Cron表达式
四、Cron实战案例
五、多线程案例
一、定时任务的理解
定时任务即系统在特定时间执行一段代码,它的场景应用非常广泛:
- 购买游戏的月卡会员后,系统每天给会员发放游戏资源。
- 管理系统定时生成报表。
- 定时清理系统垃圾。
定时任务的实现主要有以下几种方式:
- Java自带的java.util.Timer类,这个类允许调度一个java.util.TimerTask任务。使用这种方式可以让程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
- Quartz。这是一个功能比较强大的的调度器,可以让程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。
- Spring3.0以后自带Spring Task,可以将它看成一个轻量级的Quartz,使用起来比 Quartz简单许多,在课程中我们使用Spring Task实现定时任务
二、入门案例
创建SpringBoot项目,在启动类开启定时任务。
也就是在启动类上方添加@EnableScheduling注解即可开启定时任务,代码如下:
package com.example.springboottaskdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class SpringboottaskdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringboottaskdemoApplication.class, args);
}
}
编写定时任务类
@Component
public class MyTask {
// 定时任务方法,每秒执行一次
@Scheduled(cron="* * * * * *")
public void task1() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
启动项目,定时任务方法按照配置定时执行。
OK,果然如此,每隔一秒输出当前时间
@Scheduled写在方法上方,指定该方法定时执行。常用参数如下:
- cron:cron表达式,定义方法执行的时间规则。
- fixedDelay:任务立即执行,之后每隔多久执行一次,单位是毫秒,上一次任务结束后计算下次执行的时间。
OK,先来一个案例,代码如下:任务结束后每隔五秒执行一次
// 立即执行,任务结束后五秒执行一次
@Scheduled(fixedDelay = 5000)
public void task1() throws InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
System.out.println("Task1: "+sdf.format(new Date()));
}
效果如下:
OK,果然如此,注意这个是任务结束后每隔五秒,如果方法中间加了一个sleep方法,那么执行时间还要加上sleep里面的值,比如说中间加了一个sleep(1000),那么就会每隔6秒执行一次。
fixedRate:任务立即执行,之后每隔多久执行一次,单位是毫秒,上一次任务开始后计算下次执行的时间。
案例如下,代码如下:
// 立即执行,之后每五秒执行一次
@Scheduled(fixedRate = 5000)
public void task2() throws InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
// 没有影响五秒输出一次
Thread.sleep(1000);
System.out.println("Task2: "+sdf.format(new Date()));
}
OK,看如下执行代码确实是不受到sleep影响的
initialDelay:项目启动后不马上执行定时器,根据initialDelay的值延时执行。 为了突出刚刚说的fixedDelay会受到sleep影响,这里配合fixedDelay来结合测试演示一下:
代码如下:
// 项目启动后三秒执行,之后每六秒执行一次
@Scheduled(fixedDelay = 5000,initialDelay = 3000)
public void task3() throws InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
// 没有影响五秒输出一次
Thread.sleep(1000);
System.out.println("Task3: "+sdf.format(new Date()));
}
OK,看运行结果也是隔了三秒才出现第一次打印时间,并且打印时间是隔六秒打印一次
三、Cron表达式
Spring Task依靠Cron表达式配置定时规则。Cron表达式是一个字符串,分为6或7个域,每一个域代表一个含义,以空格隔开。有如下两种语法格式:
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- Seconds Minutes Hours DayofMonth Month DayofWeek
Seconds(秒):域中可出现 , – * / 四个字符,以及0-59的整数
- * :表示匹配该域的任意值,在Seconds域使用 * ,表示每秒钟都会触发
- , :表示列出枚举值。在Seconds域使用 5,20 ,表示在5秒和20秒各触发一次。
- – :表示范围。在Seconds域使用 5-20 ,表示从5秒到20秒每秒触发一次
- / :表示起始时间开始触发,然后每隔固定时间触发一次。在Seconds域使用 5/20 , 表示5秒触发一次,25秒,45秒分别触发一次。
Minutes(分):域中可出现 , – * / 四个字符,以及0-59的整数
Hours(时):域中可出现 , – * / 四个字符,以及0-23的整数
DayofMonth(日期):域中可出现 , – * / ? L W C 八个字符,以及1-31的整数
- C :表示和当前日期相关联。在DayofMonth域使用 5C ,表示在5日后的那一天触发,且每月的那天都会触发。比如当前是10号,那么每月15号都会触发。
- L :表示最后,在DayofMonth域使用 L ,表示每个月的最后一天触发。
- W :表示工作日,在DayofMonth域用 15W ,表示最接近这个月第15天的工作日触发,如果15号是周六,则在14号即周五触发;如果15号是周日,则在16号即周一触发;如果15号是周二则在当天触发。
注:
- 该用法只会在当前月计算,不会到下月触发。比如在DayofMonth域用 31W ,31号是周日,那么会在29号触发而不是下月1号。
- 在DayofMonth域用 LW ,表示这个月的最后一个工作日触发。
Month(月份):域中可出现 , – * / 四个字符,以及1-12的整数或JAN-DEC的单词缩写
DayofWeek(星期):可出现 , – * / ? L # C 八个字符,以及1-7的整数或SUN-SAT 单词缩写,1代表星期天,7代表星期六
- C :在DayofWeek域使用 2C ,表示在2日后的那一天触发,且每周的那天都会触发。比如当前是周一,那么每周三都会触发。
- L :在DayofWeek域使用 L ,表示在一周的最后一天即星期六触发。在DayofWeek域使用 5L ,表示在一个月的最后一个星期四触发。
- # :用来指定具体的周数, # 前面代表星期几, # 后面代表一个月的第几周,比如 5#3 表示一个月第三周的星期四。
- ? :在无法确定是具体哪一天时使用,用于DayofMonth和DayofWeek域。例如在每月的20日零点触发1次,此时无法确定20日是星期几,写法如下: 0 0 0 20 * ? ;或者在每月的最后一个周日触发,此时无法确定该日期是几号,写法如下: 0 0 0 ? * 1L
Year(年份):域中可出现 , – * / 四个字符,以及1970~2099的整数。该域可以省略,表示每年都触发。
四、Cron实战案例
下面有常用的案例,大家可以参考一下
含义 | 表达式 |
---|---|
每隔5分钟触发一次 | 0 0/5 * * * * |
每小时触发一次 | 0 0 * * * * |
每天的7点30分触发 | 0 30 7 * * * |
周一到周五的早上6点30分触发 | 0 30 6 ? * 2-6 |
每月最后一天早上10点触发 | 0 0 10 L * ? |
每月最后一个工作日的18点30分触发 | 0 30 18 LW * ? |
2030年8月每个星期六和星期日早上10点触发 | 0 0 10 ? 8 1,7 2030 |
每天10点、12点、14点触发 | 0 0 10,12,14 * * * |
朝九晚五工作时间内每半小时触发一次 | 0 0 0/30 9-17 ? * 2-6 |
每周三中午12点触发一次 | 0 0 12 ? * 4 |
每天12点触发一次 | 0 0 12 * * * |
每天14点到14:59每分钟触发一次 | 0 * 14 * * * |
每天14点到14:59每5分钟触发一次 | 0 0/5 14 * * * |
每天14点到14:05每分钟触发一次 | 0 0-5 14 * * * |
每月15日上午10:15触发 | 0 15 10 15 * ? |
每月最后一天的上午10:15触发 | 0 15 10 L * ? |
每月的第三个星期五上午10:15触发 | 0 15 10 ? * 6#3 |
好啦,通过这些大家应该就可以领悟了
五、多线程案例
Spring Task定时器默认是单线程的,如果项目中使用多个定时器,使用一个线程会造成效率低下。
比如说我们设置了两个定时任务,那么因为Spring Task是单线程,如果在第一个定时任务加了一个sleep方法,那么会等第一个方法响应后在执行第二个任务,就很浪费cpu运行时间。代码如下:
@Scheduled(cron = "* * * * * *")
public void task1() throws InterruptedException {
System.out.println(Thread.currentThread().getId()+"线程执行任务1 - "+new SimpleDateFormat("HH:mm:ss").format(new Date());
Thread.sleep(5000);
}
@Scheduled(cron = "* * * * * *")
public void task2() throws InterruptedException {
System.out.println(Thread.currentThread().getId()+"线程执行任务2 - "+new SimpleDateFormat("HH:mm:ss").format(new Date());
}
执行效果如下:可以看到是先执行了任务2,但是他们都要隔五秒才能运行一次,因为通过线程号可以知道这是同一个线程。
因此任务1较浪费时间,会阻塞任务2的运行。此时我们可以给SpringTask配置线程池。代码如下:
package com.example.springboottaskdemo;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 创建线程池,设置五个线程
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(4));
}
}
这样就不会出现阻塞问题了,因为两个任务不是同一个线程,接下来我们再次运行看看:
执行效果如上,确实不会影响到任务2的运行,但是如果定时任务过多,超过了配置的线程池的线程数量还是会运行错乱。
Ok,SpringBoot到这里就完结撒花了。