简介
说明
本文用示例介绍SpringBoot的@Scheduled注解的用法。
执行时间的配置
在方法上使用@Scheduled注解来设置任务的执行时间,并且使用三种属性配置方式:
项 | fixedRate | fixedDelay | cron |
描述 | 每隔多少时间就启动任务,不管该任务是否启动完成。单位:默认为毫秒 | 每次执行任务完成之后间隔多久再次执行该任务。单位:默认为毫秒 | 根据cron表达式执行 |
执行时机 | 项目启动时即开始 | 项目启动时即开始 | 根据cron表达式执行 |
initialDelay:指定第一次执行的延时时间。
如:@Scheduled(initialDelay = 1000, fixedRate = 3000) :第一次在延迟1秒后执行,之后按fixedRate的规则每 3 秒执行一次。
配置文件中指定cron
假如application.yml中指定:custom.schedule.cron.task1: 0/5 * * * * *
代码中可以这么写:@Scheduled(cron = “${custom.schedule.cron.task1}”)
配置文件中指定fixedDelay
假如application.yml中指定:custom.schedule.fixed-delay.task11: 1000
代码中可以这么写:@Scheduled(fixedDelayString = “${custom.schedule.fixed-delay.task1}”)
基础实例
前提条件
添加SpringBoot依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
启用定时器(主类添加@EnableScheduling)
@SpringBootApplication @EnableScheduling public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
示例1
@Component public class MyTest { @Scheduled(cron = "0/5 * * * * *") public void scheduled() { System.out.println("this is cron"); } }
执行结果
this is cron: 1589472135001
示例2
@Component public class MyTest { @Scheduled(cron = "0/5 * * * * *") public void scheduled1() { System.out.println("this is cron: " + System.currentTimeMillis()); } @Scheduled(fixedRate = 5000) public void scheduled2(){ System.out.println("this is fixedRate: " + System.currentTimeMillis()); } @Scheduled(fixedDelay = 5000) public void scheduled3(){ System.out.println("this is fixedDelay: " + System.currentTimeMillis()); } }
执行结果(它们是串行执行的)
this is cron: 1589472145001 this is fixedRate: 1589472146341 this is fixedDelay: 1589472146342 this is cron: 1589472150002 this is fixedRate: 1589472151342 this is fixedDelay: 1589472151343 this is cron: 1589472155001 this is fixedRate: 1589472156342 this is fixedDelay: 1589472156343
单线程问题
问题描述
简介
@Scheduled默认是单线程执行的,如果前一个任务执行时间过长,则后一个任务会被延迟,示例如下:
启动类
package com.knife.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
测试类
package com.knife.example.schedule; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component public class DemoSchedule { @Scheduled(cron = "0/1 * * * * ?") public void task1() { log.info("task1"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Scheduled(cron = "0/1 * * * * ?") public void task2() { log.info("task2"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }
执行结果:(前一个任务执行完,才会执行下一个任务)
2024-07-07 14:58:39.013 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 14:58:44.020 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 14:58:49.035 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 14:58:54.038 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 14:58:59.050 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 14:59:04.054 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 14:59:09.067 INFO 19280 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1
解决方案1:配置线程数量
配置定时任务线程池的线程个数。
修改application.yml
spring: task: scheduling: pool: size: 3 shutdown: # 定时线程池关闭时等待所有任务完成 await-termination: true
或者:
package com.knife.example.schedule; 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 ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(3)); } }
结果:并行执行,每个任务都会等上一个任务结束再执行。
2024-07-07 15:03:33.007 INFO 11452 --- [ scheduling-2] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 15:03:33.007 INFO 11452 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:03:39.003 INFO 11452 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 15:03:39.003 INFO 11452 --- [ scheduling-2] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:03:45.014 INFO 11452 --- [ scheduling-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:03:45.014 INFO 11452 --- [ scheduling-3] com.knife.example.schedule.DemoSchedule : task2
解决方案2:自定义定时任务的线程池
@Scheduled的默认线程池是:ThreadPoolTaskScheduler这个Bean,最终调用到ThreadPoolExecutor的runWorker(Worker w)。
如果自定义了TaskScheduler,那么就会使用你自定义的,不会使用默认的这个ThreadPoolTaskScheduler。
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; @Slf4j @Configuration public class ScheduledConfig { private Integer poolSize = 10; @Bean public ThreadPoolTaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setThreadFactory(new CustomizableThreadFactory("my-scheduled-task-")); // 设置线程池大小 scheduler.setPoolSize(poolSize); // 线程名称前缀 scheduler.setThreadNamePrefix("scheduled-"); scheduler.setAwaitTerminationSeconds(60); scheduler.setWaitForTasksToCompleteOnShutdown(true); // 自定义RejectedExecutionHandler scheduler.setRejectedExecutionHandler(this::rejectHandler); return scheduler; } private void rejectHandler(Runnable r, ThreadPoolExecutor executor) { log.error("定时任务线程池饱和"); // executor.getQueue().clear(); // executor.purge(); } }
结果跟上边一样。
解决方案3:异步
用法:启动类加@EnableAsync,@Scheduled类或者方法上加@Async。注意:此法会使 fixedDelay策略失效。
@Async会开启异步,使用新线程去执行任务。详见:Spring-@Async异步执行的方法 – 自学精灵。
启动类:
package com.knife.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @EnableAsync @EnableScheduling @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
测试类:
package com.knife.example.schedule; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component @Async public class DemoSchedule { @Scheduled(cron = "0/1 * * * * ?") public void task1() { log.info("task1"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Scheduled(cron = "0/1 * * * * ?") public void task2() { log.info("task2"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果:并行执行,真正按照cron一秒一次!
2024-07-07 15:08:34.010 INFO 14068 --- [ task-2] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 15:08:34.010 INFO 14068 --- [ task-1] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:08:35.015 INFO 14068 --- [ task-3] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:08:35.015 INFO 14068 --- [ task-4] com.knife.example.schedule.DemoSchedule : task2 2024-07-07 15:08:36.005 INFO 14068 --- [ task-5] com.knife.example.schedule.DemoSchedule : task1 2024-07-07 15:08:36.005 INFO 14068 --- [ task-6] com.knife.example.schedule.DemoSchedule : task2
请先
!