Spring框架分别通过TaskExecutor和TaskScheduler接口为任务提供异步执行和调度。
ThreadPoolTaskScheduler(继承自TaskScheduler) | ThreadPoolTaskExecutor(继承自TaskExecutor) | 备注 | |
---|---|---|---|
含义 | 任务调度器,定时任务 | 线程池执行器,异步任务 | |
用途 | 主要用于在指定的时间间隔内执行任务或定时任务 | 用于执行异步任务和多线程任务 | |
线程池 | 基于java.util.concurrent.ScheduledExecutorService | 基于 java.util.concurrent.ThreadPoolExecutorthreadPool | |
默认配置 | springboot默认线程池大小为1 | springboot默认线程池大小8 | |
注解 | @EnableScheduling @Scheduled | @EnableAsync @EnableScheduling @Async @Scheduled | 1注解的方法必须是public方法 2.方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。 3. 异步方法使用注解@Async的返回值只能为void或者Future |
函数返回值 | 函数可以有返回值 | 函数没有返回值 | |
异步执行 | 单个任务同步执行。 当执行时间大于我们间隔时间时,上一次任务执行完成后才会开始下一个任务。 | 非阻塞异步执行。 每次执行定时任务都会新开一个线程,即使之前的任务没有完成。 |
定时任务会创建线程池ScheduledThreadPoolExecutor,用于执行任务。springboot默认Scheduler线程池corePoolSize=1
spring定时任务使用非常简单,只需要添加两个注解@EnableScheduling,@Scheduled
1.@EnableScheduling:在spring管理的类上添加都可以,通常添加在启动类上
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); }
}
2. @Scheduled: 在定时任务的实现方法上添加该注解(@Scheduled注解的详细介绍见4.?@Scheduled参数详解)
@Slf4j
@Service
public class ScheduledTask {
@Scheduled(cron ="* * * * * *")
public int ScheduledTaskOne() throws InterruptedException {
log.info("------- ScheduledTaskOne start...");
Thread.sleep(10000L);
log.info("------- ScheduledTaskOne done.");
return 1;
}
}
如上任务配置了每秒运行一次。由于函数中执行了 Thread.sleep(10000L),可以知道该任务执行完成大约10秒。
启动服务,可以看到如下log:
2023-10-17 11:38:00.012 INFO 18620 --- [ scheduling-1] com.shirley.spring.tasks.ScheduledTask : ------- ScheduledTaskOne start...
2023-10-17 11:38:10.024 INFO 18620 --- [ scheduling-1] com.shirley.spring.tasks.ScheduledTask : ------- ScheduledTaskOne done.
2023-10-17 11:38:11.002 INFO 18620 --- [ scheduling-1] com.shirley.spring.tasks.ScheduledTask : ------- ScheduledTaskOne start...
2023-10-17 11:38:21.004 INFO 18620 --- [ scheduling-1] com.shirley.spring.tasks.ScheduledTask : ------- ScheduledTaskOne done.
2023-10-17 11:38:22.003 INFO 18620 --- [ scheduling-1] com.shirley.spring.tasks.ScheduledTask : ------- ScheduledTaskOne start...
从log中记录的时间可以看出,虽然@Scheduled配置了每秒启动一次,但第二次执行ScheduledTaskOne还是会等到第一次执行完毕后才运行,而不是根据cron配置每秒运行一次。这和@Async的行为是不同的!
2.2.1 查看默认配置
spring提供了一些配置项对scheduler进行配置。这里介绍一个查看spring配置项的方法。
1. 通过IDEL(如Intellij IDEA)找到依赖项中的spring-boot-autoconfigure:***,这个就是springboot自动配置模块。
2. 在org.springframework.boot.autoconfigure.task package下,有两个*Properties类,这两个类就是配置项对应的Bean。TaskSchedulingProperties为Scheduler的配置,TaskExecutionProperties为异步任务的相关配置。
3. 查看TaskExecutionProperties源码(部分代码如下),可以看出scheduler相关配置以spring.task.scheduling 开头。springboot会通过类中各属性的set方法给属性赋值。例如,配置项spring.task.scheduling.threadNamePrefix 会通过TaskSchedulingProperties.setThreadNamePrefix(String threadNamePrefix) 函数复制给threadNamePrefix
// 配置项前缀
@ConfigurationProperties("spring.task.scheduling")
public class TaskSchedulingProperties {
private final Pool pool = new Pool();
private final Shutdown shutdown = new Shutdown();
// 线程名称前缀
private String threadNamePrefix = "scheduling-";
public static class Pool {
// 默认线程池大小为1
private int size = 1;
}
public static class Shutdown {
// 当线程池调用shutdown时,是否等待任务完成
private boolean awaitTermination;
// 等待任务完成最长时间。
private Duration awaitTerminationPeriod;
}
}
2.2.2 修改配置
根据TaskSchedulingProperties类,我们可以得到任务调度相关配置项有哪些。如下是一份完整的配置(如下配置文件为yaml格式,如果配置文件为.properties, 请自行将yaml转为properties。)
spring:
task:
scheduling:
# 线程名前缀
threadNamePrefix: shirley-scheduler-
pool:
# 线程池大小。corePoolSize
size: 5
shutdown:
# 当线程池shutdown时,是否等待已分配的任务执行完成
awaitTermination: true
# shutdown后,等待任务完成
的最长时间
awaitTerminationPeriod: "2s"
在日常开发中,通常我们只需要配置spring.task.scheduling.pool.size
定时任务会创建线程池用于执行任务。springboot 默认Async线程池 corePoolSize=8
1. 添加注解@EnableAsync,@EnableScheduling启动异步任务
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. 在定时任务的实现方法上添加该注解@Async,@Scheduled
@Slf4j
@Service
public class AsyncTask {
@Async
@Scheduled(cron = "* * * * * *")
public void AsyncTaskOne() throws InterruptedException {
log.info("------- AsyncTaskOne start...");
Thread.sleep(10000L);
log.info("------- AsyncTaskOne done.");
}
}
如上任务配置了每秒运行一次。由于函数中执行了 Thread.sleep(10000L),可以知道该任务执行完成大约10秒。
启动服务,可以看到如下log
2023-Oct-18 12:34:07.022 INFO [task-1] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:08.001 INFO [task-2] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:09.013 INFO [task-3] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:10.008 INFO [task-4] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:11.008 INFO [task-5] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:12.005 INFO [task-6] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:13.003 INFO [task-7] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:14.016 INFO [task-8] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:17.035 INFO [task-1] c.s.s.t.AsyncTask:16 - ------- AsyncTaskOne done.
2023-Oct-18 12:34:17.037 INFO [task-1] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
从log中可以看出,同时有8个相同的任务同时执行,线程分别为[task-1] ~ [task8]。
通过查看spring-boot-autoconfigure:***.jar 下类 org.springframework.boot.autoconfigure.task.TaskExecutionProperties,可以得知异步任务相关配置项。配置项说明如下:
spring:
task:
execution:
# 线程名前缀
threadNamePrefix: shirleyTask-
# 线程池相关配置
pool:
# 对应线程池的corePoolSize
coreSize: 8
# 对应线程池 workQueue的大小。
queueCapacity: 100
# 对应线程池的maxPoolSize
maxSize: 100
# 对应线程池 keepAliveTime,TimeUnit.SECONDS
keepAlive: 60s
# 线程池coreThread空闲时间超过keepAlive后,是否terminated。
allowCoreThreadTimeout: true
shutdown:
# 当线程池shutdown时,是否等待已分配的任务执行完成
awaitTermination: false
# shutdown后,等待任务完成
的最长时间
awaitTerminationPeriod: "10s"
如果不想用spring默认的线程池,比如希望更改默认线程池拒绝策略(AbortPolicy),也可以自定义线程池。
自定义线程池只需要两步:
1. 定义一个TaskExecutor类型的spring bean。如下例子中,定义了名为"shirleyExecutor" 的TaskExecutor
@Bean("shirleyExecutor")
public Executor shirleyExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//最大线程数
executor.setMaxPoolSize(5);
//核心线程数
executor.setCorePoolSize(5);
//任务队列的大小
executor.setQueueCapacity(10);
//线程前缀名
executor.setThreadNamePrefix("shirley-exe-");
//线程存活时间
executor.setKeepAliveSeconds(10);
/**
* 拒绝处理策略
* CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
* AbortPolicy():直接抛出异常。
* DiscardPolicy():直接丢弃。
* DiscardOldestPolicy():丢弃队列中最老的任务。
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
2. 在@Async注解上指定自定义的Bean
@Async("shirleyExecutor")
@Scheduled(cron = "* * * * * *")
public void shirleyExeTask() throws InterruptedException {
log.info("------- shirleyExeTask start...");
Thread.sleep(10000L);
log.info("------- shirleyExeTask done.");
}
注意:Async执行器方法只能返回void或Further
运行后,可以从log看出已经使用了自定义的Executor。在这个例子中,threadName已经变成了shirley-exe-*
2023-Oct-18 17:21:39.014 INFO [shirley-exe-1] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...
2023-Oct-18 17:21:40.011 INFO [shirley-exe-2] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...
2023-Oct-18 17:21:41.010 INFO [shirley-exe-3] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...
spring 异步任务Async和定时任务Scheduler都可以通过@Scheduled注解进行调度。@Scheduled注解有三种方式配置调度频率:
注意,如果通过cron,fixedRate配置执行频率,Scheduler和Async的行为不同:
cron表达式一共6位,分别表示:秒,分,时,日期,月,星期
┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *
* 第一位,表示秒,取值0-59
* 第二位,表示分,取值0-59
* 第三位,表示小时,取值0-23
* 第四位,日期天/日,取值1-31
* 第五位,日期月份,可以为:1-12;也可以表示为英文月份缩写:JAN~DEC
* 第六位,星期,可以为:0-7(0或7表示星期天); 也可以为英文星期缩写:MON~SUN
表达式说明:
(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年...
(?)问号:问号只能出现在日期和星期这两个位置,表示这个位置的值不确定,每天3点执行,所以第六位星期的位置,我们是不需要关注的,就是不确定的值。同时:日期和星期是两个相互排斥的元素,通过问号来表明不指定值。比如,1月10日,比如是星期1,如果在星期的位置是另指定星期二,就前后冲突矛盾了。
(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四
(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60 另:*/y,等同于0/y
英文月份/星期缩写:可以用月份/星期 英文单词的前三个字母表示月份和星期几,大小写不敏感。
(L)最后一个:日期和星期如果包含L字母,表示最后一天。如日期使用“L”表示月的最后一天;“L-n”表示 n号到月末。
(LW)最后一个工作日(周一到周五,不是中国的法定工作日)
(d#n)第n个星期d
一些有用的cron表达式:
0 0 3 * * ? 每天3点执行
0 5 3 * * ? 每天3点5分执行
0 5 3 ? * * 每天3点5分执行,与上面作用相同
0 5/10 3 * * ? 每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行
0 10 3 ? * 1 每周星期天,3点10分 执行,注:1表示星期天
0 10 3 ? * 1#3 每个月的第三个星期一执行,#号只能出现在星期的位置
0 0 9-17 * * MON-FRI 每月周一到周五9点到17点
0 0 0 25 12 ? 每个圣诞节(12月25日)0点
0 0 0 L * * 每个月最后一天0点
0 0 0 L-3 * * 每月3号到月底的0点
0 0 0 1W * * 每月第一个工作日的0点
0 0 0 LW * * 每月最后一个工作日的0点
0 0 0 * * 5L 每个月最后一个周五的0点
0 0 0 * * THUL 每月最后一个星期四0点
0 0 0 ? * 5#2 每月第二个星期五0点
0 0 0 ? * MON#1 每月第一个星期一0点
4.1.1 cron宏
由于0 0 * * * * 的可读性比较差,spring定义了一些宏,替代常见的cron表达式,比如:@Scheduled(cron = "@hourly").spring支持的宏如下表:
宏 | 含义 |
---|---|
@yearly 或 @annually | 每年一次 (0 0 0 1 1 *) |
@monthly | 每月一次(0 0 0 1 * *) |
@weekly | 每周一次 (0 0 0 * * 0) |
@daily (or @midnight) | 每天一次 (0 0 0 * * *) |
@hourly | 每小时一次 (0 0 * * * *) |
示例代码:
// 每小时运行一次
@Scheduled(cron = "@hourly")
public void shirleyExeTask() throws InterruptedException {
log.info("------- shirleyExeTask start...");
Thread.sleep(10000L);
log.info("------- shirleyExeTask done.");