schedule
springboot 常见的两个开发场景:
1、rest 接口服务:前端接口服务,后台OpenApi接口服务
2、定时任务:定时数据集成,定时数据计算,定时扫描redis,发邮件短信定时告警等等。
Java线程框架非常复杂,从一般的悲观锁,乐观锁,AQS等等,设计了大量的类,容器,但是对于使用场景尤其非互联网项目,做CICD,自动化运维,自动化运营,OA类项目,大多情况不会使用原生的Timer,Schedule。
对于定时任务,分布式定时任务,以前常用Quarz框架,XXL-JOB,其实轻量级的直接使用@Schedule就够了。
1、第一个定时任务
package com.wht.spring.boot.schedule;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class HelloSchedule {
@Scheduled(fixedRate = 2000L)
public void sayHello(){
System.out.println(Thread.currentThread().getName()+"-sayHello!");
}
}
查看console没有任何反应
-
启动类必须添加 @EnableScheduling
springboot核心特性组件化和自动装配,为了精简并不是把所有模块都装配上,所以要开启定时任务功能,手工添加注解
-
@Component 调度必须是组件,才能自动装配进context
-
sayHello() 必须是无参方法 看下源码就明白了,org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.class#processScheduled
-
@Scheduled(fixedRate = 2000L) 必须,并且必须又一个表示频率的值也是看上述源码
@Scheduled的定时策略有如下几种方式:
- cron 定时任务表达式:@Scheduled(cron = "* * * * * * ") 每秒
- fixedRate: 定时多久执行一次(上一次开始执行时间点后xx秒再次执行;)
- fixedDelay: 上一次执行结束时间点后xx秒再次执行
2、单个定时任务的深入测试
补充上@EnableScheduling之后就跑起来了
pool-1-thread-1-sayHello!
pool-1-thread-1-sayHello!
pool-1-thread-1-sayHello!
从打印的进程名称就能看出来,使用的pool思想
2.1、执行时间大于制定频率
@Component
public class HelloSchedule {
public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Scheduled(fixedRate = 1000L)
public void sayHello() throws InterruptedException {
System.out.println(df.get().format(new Date())+"\t"+Thread.currentThread().getName() + "-sayHello!");
Thread.sleep(5000);
}
}
2022-09-29 10:01:16 pool-1-thread-1-sayHello!
2022-09-29 10:01:21 pool-1-thread-1-sayHello!
2022-09-29 10:01:26 pool-1-thread-1-sayHello!
2022-09-29 10:01:31 pool-1-thread-1-sayHello!
2022-09-29 10:01:36 pool-1-thread-1-sayHello!
2022-09-29 10:01:41 pool-1-thread-1-sayHello!
2022-09-29 10:01:46 pool-1-thread-1-sayHello!
2022-09-29 10:01:51 pool-1-thread-1-sayHello!
2022-09-29 10:01:56 pool-1-thread-1-sayHello!
明显会阻塞,而非根据频率定时生成任务,放到线程池并行执行。
2.2、如果任务异常了调度会不会挂
@Component
public class HelloSchedule {
public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Scheduled(cron = "* * * * * * ")
public void sayHello() throws InterruptedException {
System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello!");
Thread.sleep(5000);
System.out.println(1 / 0);
}
}
实验证明不会挂,打印异常之后继续跑
org.springframework.scheduling.support.ScheduledMethodRunnable.class#run()
这个方法把异常捕获处理了,不会导致程序中断。
2022-09-29 10:05:46 pool-1-thread-1-sayHello!
2022-09-29 10:05:51.013 ERROR 16684 --- [pool-1-thread-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task.
java.lang.ArithmeticException: / by zero
at com.wht.spring.boot.schedule.HelloSchedule.sayHello(HelloSchedule.java:17) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_341]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_341]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_341]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_341]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93) [spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_341]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_341]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_341]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_341]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_341]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_341]
at java.lang.Thread.run(Thread.java:750) [na:1.8.0_341]
3、多个任务之间是不是并行的
真正的项目一般调度包是独立的,往往代表项目的任务模块,自然不至于一个任务。
下面有三个任务会怎么执行
@Component
public class HelloSchedule {
public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Scheduled(cron = "* * * * * * ")
public void sayHello() throws InterruptedException {
System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello!");
Thread.sleep(10000);
}
@Scheduled(cron = "* * * * * * ")
public void sayHello1() throws InterruptedException {
System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello1!");
Thread.sleep(5000);
}
@Scheduled(cron = "* * * * * * ")
public void sayHello2() throws InterruptedException {
System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello2!");
}
}
看下执行结果:
2022-09-29 10:14:09 pool-1-thread-1-sayHello1!
2022-09-29 10:14:14 pool-1-thread-1-sayHello!
2022-09-29 10:14:24 pool-1-thread-1-sayHello2!
2022-09-29 10:14:24 pool-1-thread-1-sayHello1!
sayHello2 并不能疫苗执行一次,三个任务好像只有一个线程,串起来了。
这就是为什么有些新同事开发了调度,结果生产经常不跑原因之一。
org.springframework.scheduling.config.SchedulerBeanDefinitionParser.class#doParse
pool-size
org\springframework\scheduling\config\spring-task.xsd 中说明了default-value 1
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
3.1、方案一自己提供执行器,替代掉原始的newSingleThreadScheduledExecutor
这有个好处,就是可以自定义很多信息,便于个性化
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
}
执行结果:
2022-09-29 10:32:28 pool-1-thread-1-sayHello!
2022-09-29 10:32:28 pool-1-thread-2-sayHello1!
2022-09-29 10:32:28 pool-1-thread-3-sayHello2!
2022-09-29 10:32:29 pool-1-thread-3-sayHello2!
2022-09-29 10:32:30 pool-1-thread-4-sayHello2!
可以看到线程有多个了,多个任务之间可以并行了
该配置的自定义配置以 spring.task.scheduling
开头。同时它需要在任务执行器配置 TaskExecutionAutoConfiguration
配置后才生效。我们只需要在中对其配置属性 spring.task.execution
相关属性配置即可。
Spring Boot 的 application.properties
中相关的配置说明:
# 任务调度线程池
# 任务调度线程池大小 默认 1 建议根据任务加大
spring.task.scheduling.pool.size=1
# 调度线程名称前缀 默认 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 线程池关闭时等待所有任务完成
spring.task.scheduling.shutdown.await-termination=
# 调度线程关闭前最大等待时间,确保最后一定关闭
spring.task.scheduling.shutdown.await-termination-period=
# 任务执行线程池配置
# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
# 核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数 根据任务定制
spring.task.execution.pool.max-size=
# 线程池 队列容量大小
spring.task.execution.pool.queue-capacity=
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-
以上配置已经很全了,但是还是单线程阻塞,springboot升级到
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
实验了才知道为什么