目录
定时任务
工作中有需要应用到定时任务的场景,一天一次,一周一次,一月一次,一年一次,做日报,周报,月报,年报的统计,以及信息提醒,等,spring boot 提供了一个两种方式实现定时任务。
一、静态定时任务—基于注解
SpringBoot
中的@Scheduled
注解为定时任务提供了一种很简单的实现,只需要在注解中加上一些属性,例如fixedRate
、fixedDelay
、cron
(最常用)等等,并且在启动类上面加上@EnableScheduling
注解,就可以启动一个定时任务了。基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
程序中若需要在某个时间循环执行某项任务,就可以使用@Scheduled
定时器
1、主要涉及的注解:
@EnableScheduling //开启定时任务,在配置类上使用,为了方便,可以用在项目启动类上
@Scheduled //执行任务间隔设置,来声明这是一个任务,包括 cron,fixDelay,fixRate 等类型
(1)开启定时任务
SpringBoot
项目在项目启动类上添加 @EnableScheduling
注解即可开启定时任务管理。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling //开启定时任务
public class ScheduledDemoApplication{
public static void main(String[] args){
SpringApplication.run(ScheduledDemoApplication.class, args);
}
}
(2)创建定时任务
创建定时任务,并使用 @Scheduled 注解。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 定时任务的使用
**/
@Component //注入到容器
public class Task{
@Scheduled(cron="0/5 * * * * ? ") //每5秒执行一次
public void execute(){
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式
System.out.println("执行第"+ i++ +"次定时任务_" + df.format(new Date()));
}
}
可通过在线生成Cron
表达式的工具:http://cron.qqe2.com/ 来生成自己想要的表达式。
以上,基于@Scheduled 注解来实现定时任务已完成;以下介绍@Scheduled 注解各个参数的使用
2、@Scheduled注解各大参数的作用
发现有这么几个参数:
cron
、zone
、fixedDelay
、fixedDelayString
、fixedRate
、fixedRateString
、initialDelay
、initialDelayString
用法分别如下:
@Scheduled注解类的源码
package org.springframework.scheduling.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = "-";
String cron() default "";
String zone() default "";
long fixedDelay() default -1L;
String fixedDelayString() default "";
long fixedRate() default -1L;
String fixedRateString() default "";
long initialDelay() default -1L;
String initialDelayString() default "";
}
2.1、cron
相关参数意义
一个cron
表达式有至少6个(也可能7个)有空格分隔的时间元素。
按顺序依次为
- 秒(0~59) 例如0/5表示每5秒
- 分钟(0~59)
- 小时(0~23)
- 天(月)(0~31,但是你需要考虑你月的天数)
- 月(0~11)
- 天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
- 年份(1970-2099)——@Scheduled是不支持的,spring quartz支持
0 0 10,14,16 * * ? | 每天上午10点,下午2点,4点 |
---|---|
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时 |
0 0 12 ? * | WED 表示每个星期三中午12点 |
“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期间的每1分钟触发 |
“0 0/5 14 * * ?” | 在每天下午2点到下午2:55期间的每5分钟触发 |
"0 “0/5 14,18 * * ?” | 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 |
“0 0-5 14 * * ?” | 在每天下午2点到下午2:05期间的每1分钟触发 |
“0 10,44 14 ? 3 WED” | 每年三月的星期三的下午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” | 每月的第三个星期五上午10:15触发 |
有些子表达式能包含一些范围或列表
例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”
“*”字符代表所有可能的值
因此,“”在子表达式(月)里表示每个月的含义,“”在子表达式(天(星期))表示星期的每一天
“/”字符用来指定数值的增量
例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟
在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样
“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值
当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”
“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写
但是它在两个子表达式里的含义是不同的。
在天(月)子表达式中,“L”表示一个月的最后一天
在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT
如果在“L”前有具体的内容,它就具有其他的含义了
例如:“6L”
表示这个月的倒数第6天,“FRIL”
表示这个月的最一个星期五
注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题
字段 | 允许值 | 允许的特殊字符 |
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * ? / L C # |
年(可选) | 留空, 1970-2099 | , - * / |
局限性——@Scheduled的cron
无法指定执行的年份
即我们假如使用下面的定时任务
@Scheduled(cron = "0 18 10 * * ? 2021-2022")
public void testTaskWithDate() {
logger.info("测试2021.定时任务");
}
将会报下面的错误
Cron expression must consist of 6 fields (found 7 in "0 18 10 * * ? 2016-2016")
cron
表达式使用占位符
另外,cron
属性接收的cron
表达式支持占位符。
配置文件:
time:
cron: */5 * * * * *
interval: 5
每5秒执行一次:
@Scheduled(cron="${time.cron}")
void testPlaceholder1() {
System.out.println("Execute at " + System.currentTimeMillis());
}
@Scheduled(cron="*/${time.interval} * * * * *")
void testPlaceholder2() {
System.out.println("Execute at " + System.currentTimeMillis());
}
2.2、zone
时区,接收一个 java.util.TimeZone#ID
。cron
表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段我们一般留空。
2.3、fixedDelay
、fixedDelayString
这两个参数意思是相同的,都是表示:上一次执行完毕时间点之后多长时间再执行,区别是:后者支持占位符。
上一次执行完毕时间点之后多长时间再执行。如:
@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行
@Scheduled(fixedDelayString = "5000") //上一次执行完毕时间点之后5秒再执行
fixedDelayString
使用占位符
#配置文件
time:
fixedDelay: 5000
@Scheduled(fixedDelayString = "${time.fixedDelay}")
2.4、fixedRate
、fixedRateString
这两个参数意思是相同的,都是表示:上一次开始执行时间点之后多长时间再执行,区别是:后者支持占位符。
上一次开始执行时间点之后多长时间再执行。如:
@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行
@Scheduled(fixedRateString = "5000") //上一次开始执行时间点之后5秒再执行
fixedRateString
使用占位符
#配置文件
time:
fixedDelay: 5000
@Scheduled(fixedRateString = "${time.fixedDelay}")
2.4、initialDelay
、initialDelayString
这两个参数意思是相同的,都是表示:第一次延迟多长时间后再执行,区别是:后者支持占位符。
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
initialDelayString
使用占位符 同上
二、动态定时任务—基于接口
不需要重启项目就能够生效,需要把
cron
表达式持久化到数据库
1、持久化到数据库
1.1、创建数据表
在MySQL
数据库中创建cron
表,并添加数据。
DROP TABLE IF EXISTS cron;
CREATE TABLE cron (
cron_id VARCHAR(30) NOT NULL PRIMARY KEY,
cron VARCHAR(30) NOT NULL
);
INSERT INTO cron VALUES ('1', '0/5 * * * * ?');
1.2、添加pom.xml
配置信息
<!-- MyBatis与SpringBoot整合依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL的JDBC数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
1.3、配置相关信息
将项目默认的application.properties
文件的后缀修改为“.yml”
,即配置文件名称为:application.yml
,并配置以下信息:
spring:
#DataSource数据源
datasource:
url: jdbc:mysql://localhost:3306/db_admin?useSSL=false&
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
#MyBatis配置
mybatis:
type-aliases-package: com.pjb.entity #别名定义
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #指定 MyBatis 所用日志的具体实现,未指定时将自动查找
map-underscore-to-camel-case: true #开启自动驼峰命名规则(camel case)映射
lazy-loading-enabled: true #开启延时加载开关
aggressive-lazy-loading: false #将积极加载改为消极加载(即按需加载),默认值就是false
lazy-load-trigger-methods: "" #阻挡不相干的操作触发,实现懒加载
cache-enabled: true #打开全局缓存开关(二级环境),默认值就是true
2、创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是
TriggerTask
,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。具体代码如下:
package com.example.springSchedule.controller;
import com.example.springbucket.mapper.CronMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
/**
* 动态定时任务配置类
*/
@Configuration //标记配置类,兼备Component的效果
@EnableScheduling //开启定时任务
public class SimpleScheduleConfig implements SchedulingConfigurer{
@Autowired
CronMapper cronMapper;
// @Override
// public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
// taskRegistrar.addTriggerTask(
// //添加任务内容(Runnable)
// () -> System.out.println("线程启动:" + LocalDateTime.now().toLocalTime()),
// //设置执行周期(Trigger)
// triggerContext -> {
// //从数据库获取执行周期
// String cron = cronMapper.getCron();
// //非空校验
// if(StringUtils.isEmpty(cron)){
// //TODO
// }
// //返回执行周期
// return new CronTrigger(cron).nextExecutionTime(triggerContext);
// }
// );
//
// }
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
Thread thread = new Thread(){
@Override
public void run() {
super.run();
System.out.println("定时发送线程启动:" + LocalDateTime.now().toLocalTime());
}
};
taskRegistrar.addTriggerTask(
thread,
triggerContext -> {
//从数据库获取执行周期
String cron = cronMapper.getCron();
//非空校验
if(StringUtils.isEmpty(cron)){
//TODO
}
//返回执行周期
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
}
通过读取数据库数据来控制定时任务的周期
注意: 如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。
因为
SchedulingConfigurer
类没有直接提供关闭定时任务的方法,所以,当我们需要停止定时任务的循环执行的时候,清空cron
表达式,通过非空校验捕获到,然后业务处理手动关闭的功能。
//清空扫描到的定时任务即可
taskRegistrar.setTriggerTasks(Maps.newHashMap());
taskRegistrar.setCronTasks(Maps.newHashMap());
taskRegistrar.setFixedRateTasks(Maps.newHashMap());
taskRegistrar.setFixedDelayTasks(Maps.newHashMap());
三、动态控制定时任务
例如项目部署上线之后,我们可能会修改定时任务的执行时间,并且停止、重启定时任务等,因为定时任务是直接写死在程序中的,修改起来不是非常的方便。所以,简单记录一下自己的一些解决方案,仅供参考。
1、针对 @Scheduled 定时任务
将 cron
表达式配在 application.yml
中:
#application.yml中的配置
scheduled:
cron: 0/5 * * * * ?
在 @Scheduled 中获取这个配置:
@Component
public class TestTask {
private static SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Scheduled(cron = "${scheduled.cron}")
public void test(){
System.out.println(dateFmt.format(new Date()) + " : 执行定时任务");
}
}
1.1、如何关闭定时任务
Spring Boot 2.1 以上的版本还提供了一种停止定时任务的方案,就是在cron
中配置 “-” 即可,你也可以在配置文件中设置这个符号:
#application.yml中的配置
scheduled:
cron: "-"
注意这里必须加上一个双引号,因为在
application.yml
中, - 是一个特殊的字符。
1.2、为定时任务设置开关
另一种方式,在配置文件中配置一个 boolean 属性,如果是 true 的话,就开启定时任务,否则不开启。
#application.yml中的配置
scheduled:
cron: 0/5 * * * * ?
enable:
scheduled: true # @Schedule 定时任务的开true/关false
其实 @Scheduled 注解,是被一个叫做
ScheduledAnnotationBeanPostProcessor
的类所拦截的,所以我们可以根据配置,决定是否创建这个 bean,如果没有这个 bean,@Scheduled 就不会被拦截,那么定时任务肯定不会执行了
**注意:这种方式,启动类上面的 @EnableScheduling 需要去掉。**不去除定时将一直生效,一直为true
(1)、创建一个 ScheduledCondtion
类
public class ScheduledCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//读取配置中的属性
return Boolean.parseBoolean(context.getEnvironment().getProperty("enable.scheduled"));
}
}
这个类的功能很简单,就是去读取配置,然后返回一个 boolean 值。
(2)、创建一个 ScheduledConfig
配置类
需要 @Conditional
注解来实现这个功能,详细了解见:
@Configuration
public class ScheduledConfig {
// 根据配置文件中的内容,决定是否创建 bean
@Conditional(ScheduledCondition.class)
@Bean
public ScheduledAnnotationBeanPostProcessor processor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
这个配置,就是以
ScheduledCondtion
为条件,决定是否创建 bean。然后,启动项目,定时任务就会执行,如果我们将配置修改为 false,则不会执行。这样的话,我们就能够很容易的启动或者关闭定时任务了,并且也可以实时修改
cron
表达式的值。
2021-01-27 11:14:35 : 执行定时任务
2021-01-27 11:14:40 : 执行定时任务
2021-01-27 11:14:45 : 执行定时任务
2021-01-27 11:14:50 : 执行定时任务
2021-01-27 11:14:55 : 执行定时任务
2021-01-27 11:15:00 : 执行定时任务
2、修改定时任务不重启项目
基于接口 SchedulingConfigurer
,这里大家要了解ScheduledTaskRegistrar
这个类
2.1、建表 管理定时任务
DROP TABLE IF EXISTS `scheduled`;
CREATE TABLE `scheduled` (
`name` varchar(20) DEFAULT NULL,
`cron` varchar(30) DEFAULT NULL,
`open` tinyint(1) DEFAULT NULL COMMENT '1开启, 其他为关闭'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of scheduled
-- ----------------------------
INSERT INTO `scheduled` VALUES ('demo1', '0/5 * * * * ?', '1');
INSERT INTO `scheduled` VALUES ('demo2', '0/10 * * * * ?', '1');
2.1、实现定时任务配置类
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* 基于接口SchedulingConfigurer的动态定时任务.
*/
@Configuration
@EnableScheduling
public abstract class BaseSchedulingConfigurer implements SchedulingConfigurer {
/**
* 定时任务周期表达式.
*/
private String cron;
/**
* 重写配置定时任务的方法.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(taskScheduler());
scheduledTaskRegistrar.addTriggerTask(
//执行定时任务
this::taskService,
//设置触发器
triggerContext -> {
//获取定时任务周期表达式
cron = getCron();
//返回执行周期
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
@Bean
public Executor taskScheduler() {
//设置线程名称
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("scheduler-pool-%d").build();
//创建线程池
return Executors.newScheduledThreadPool(3, namedThreadFactory);
}
/**
* 执行定时任务
*/
public abstract void taskService();
/**
* 获取定时任务周期表达式
*/
public abstract String getCron();
}
2.2、创建定时任务
定时任务一:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dao.CronMapper;
import com.example.demo.entity.Scheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* 定时任务一.
*/
@Configuration
public class TaskDemo extends BaseSchedulingConfigurer {
/**
* 注入mapper
*/
@Autowired
@SuppressWarnings("all")
CronMapper cronMapper;
/**
* 执行定时任务内容
*/
@Override
public void taskService() {
Integer open = getOpen();
if (1== open){
System.out.println("定时任务demo1:"
+ LocalDateTime.now()+",线程名称:"+Thread.currentThread().getName()
+ " 线程id:"+Thread.currentThread().getId());
}
}
/**
* 获取定时任务执行周期表达式
*/
@Override
public String getCron() {
QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name","demo1");
String cron = cronMapper.selectOne(queryWrapper).getCron();
return cron;
}
/**
* 得到定时任务,开关状态.
*/
public Integer getOpen() {
QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "demo1");
Integer open = cronMapper.selectOne(queryWrapper).getOpen();
return open;
}
}
定时任务二:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dao.CronMapper;
import com.example.demo.entity.Scheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* 定时任务二.
*/
@Configuration
public class TaskDemoCopy extends BaseSchedulingConfigurer {
/**
* 注入mapper
*/
@Autowired
@SuppressWarnings("all")
CronMapper cronMapper;
/**
* 执行定时任务内容
*/
@Override
public void taskService() {
Integer open = getOpen();
if (1== open){
System.out.println("定时任务demo2:"
+ LocalDateTime.now()+",线程名称:"+Thread.currentThread().getName()
+ " 线程id:"+Thread.currentThread().getId());
}
}
/**
* 获取定时任务执行周期表达式
*/
@Override
public String getCron() {
QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name","demo2");
String cron = cronMapper.selectOne(queryWrapper).getCron();
return cron;
}
/**
* 得到定时任务,开关状态.
*/
public Integer getOpen() {
QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "demo2");
Integer open = cronMapper.selectOne(queryWrapper).getOpen();
return open;
}
}
结果打印
定时任务demo1
:每五秒执行一次
定时任务demo2
: 每十秒执行一次
定时任务demo1:2021-01-27T17:45:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:10.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:45:15.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:45:20.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:20.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T17:45:25.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:45:30.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:30.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:45:35.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:40.001,线程名称:scheduler-pool-0 线程id:28
修改数据库 demo1
的执行时间为每15秒一次:
定时任务demo1
:每十五秒执行一次
定时任务demo2
: 每十秒执行一次
定时任务demo1:2021-01-27T17:58:05.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T17:58:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:58:10.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:58:15.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T17:58:20.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T17:58:30.003,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:58:30.003,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T17:58:40.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:58:45.001,线程名称:scheduler-pool-2 线程id:51
关闭demo1
的定时任务,将demo1
的open状态改为0:
定时任务demo1
:关闭
定时任务demo2
: 每十秒执行一次
定时任务demo2:2021-01-27T17:59:50.003,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:00.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T18:00:00.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:10.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T18:00:15.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:00:20.001,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:30.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:00:40.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:00:50.001,线程名称:scheduler-pool-0 线程id:28
重新打开demo1
的定时任务,将demo1
的open状态改为1:
定时任务demo2:2021-01-27T18:04:00.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:20.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:04:30.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:04:30.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:40.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:04:45.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:04:50.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:05:00.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:05:00.002,线程名称:scheduler-pool-2 线程id:51
注意:
cron
为空,new CronTrigger(cron)
将抛出空指针异常
四、多线程定时任务
两种情况,如果是多个任务之间的多线程可以配置线程池解决,还有一种就是单任务多线程可以使用
@Async
解决,可以直接指定线程池@Async("taskExecutor")
1、有坑
SpringBoot
使用@scheduled定时执行任务的时候是在一个单线程中,如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。也就是会造成一些任务无法定时执行的错觉
可以通过如下代码进行测试:
@Scheduled(cron = "0/1 * * * * ? ")
public void deleteFile() throws InterruptedException {
log.info("111delete success, time:" + new Date().toString());
Thread.sleep(1000 * 5);//模拟长时间执行,比如IO操作,http请求
}
@Scheduled(cron = "0/1 * * * * ? ")
public void syncFile() {
log.info("222sync success, time:" + new Date().toString());
}
/**输出如下:
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:13 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:18 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:19 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:24 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:25 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:25 CST 2018
上面的日志中可以明显的看到syncFile被阻塞了,直达deleteFile执行完它才执行了
而且从日志信息中也可以看出@Scheduled是使用了一个线程池中的一个单线程来执行所有任务的。
**/
/**如果把Thread.sleep(1000*5)注释了,输出如下:
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:06 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:06 CST 2018
这下正常了
**/
2、解决办法
2.1.将@Scheduled注释的方法内部改成异步执行
如下:
//当然了,构建一个合理的线程池也是一个关键,否则提交的任务也会在自己构建的线程池中阻塞
ExecutorService service = Executors.newFixedThreadPool(5);
@Scheduled(cron = "0/1 * * * * ? ")
public void deleteFile() {
service.execute(() -> {
log.info("111delete success, time:" + new Date().toString());
try {
Thread.sleep(1000 * 5);//改成异步执行后,就算你再耗时也不会印象到后续任务的定时调度了
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
@Scheduled(cron = "0/1 * * * * ? ")
public void syncFile() {
service.execute(()->{
log.info("222sync success, time:" + new Date().toString());
});
}
2.2.把Scheduled配置成成多线程执行
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//当然了,这里设置的线程池是corePoolSize也是很关键了,自己根据业务需求设定
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
/**为什么这么说呢?
假设你有4个任务需要每隔1秒执行,而其中三个都是比较耗时的操作可能需要10多秒,而你上面的语句是这样写的:
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(3));
那么仍然可能导致最后一个任务被阻塞不能定时执行
**/
}
}
2.3、使用@Async
注解
如果你还不了解@Async
的使用,可以参考我的这篇文章:如何使用 @Async 实现异步调用
/**
* 基于注解设定多线程定时任务
* @author pan_junbiao
*/
@Component
@EnableScheduling // 1.开启定时任务
@EnableAsync // 2.开启多线程
public class MultithreadScheduleTask{
@Async
@Scheduled(fixedDelay = 1000) //间隔1秒
public void first() throws InterruptedException {
System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
Thread.sleep(1000 * 10);
}
@Async
@Scheduled(fixedDelay = 2000)
public void second() {
System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
}
}
后记
springboot
定时任务源码解析
推荐参考:https://blog.csdn.net/a842699897/article/details/83790282
Java定时任务
开发中,往往遇到另起线程执行其他代码的情况,用java
定时任务接口ScheduledExecutorService
来实现。
ScheduledExecutorService
是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
注意,只有当调度任务来的时候,ScheduledExecutorService
才会真正启动一个线程,其余时间ScheduledExecutorService
都是处于轮询任务的状态。
// 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
// scheduleAtFixedRate() 每次执行时间为上一次任务开始起向后推一个时间间隔,是基于固定时间间隔进行任务调度
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
}, 5, 10, TimeUnit.SECONDS);
// scheduleWithFixedDelay() 每次执行时间为上一次任务结束起向后推一个时间间隔,取决于每次任务执行的时间长短
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
}, 5, 10, TimeUnit.SECONDS);
// 只执行一次延时任务
ScheduledExecutorService scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
scheduledThreadPoolExecutor.schedule(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!");
}, 20, TimeUnit.SECONDS);
ScheduleAtFixedRate`每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为`initialDelay,initialDelay+period,initialDelay+2*period。。。。。 ScheduleWithFixedDelay`每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:`initialDelay,initialDelay+executeTime+delay,initialDelay+2*executeTime+2*delay。。。。。
由此可见,
ScheduleAtFixedRate
是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay
取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。