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>
实验了才知道为什么
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战