SpringBoot 使用@Scheduled注解配置串行、并行定时任务

本文详细介绍Java定时任务的三种实现方式,包括Java自带的Timer类、Quartz或elastic-job框架及Spring的@Schedule注解。探讨了定时任务的调度方式,如单线程和多线程,并介绍了cron表达式的使用及如何利用延迟队列提高任务执行效率。

三种: 
1) Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。 最早的时候就是这样写定时任务的。 
2) 开源的第三方框架: Quartz 或者 elastic-job , 但是这个比较复杂和重量级,适用于分布式场景下的定时任务,可以根据需要多实例部署定时任务。 
3) 使用Spring提供的注解: @Schedule 。 如果定时任务执行时间较短,并且比较单一,可以使用这个注解。

定时任务的创建
存在两种调度方式: 单线程和多线程

串行方式

使用的注解: @Scheduled 和 @EnableScheduling

@Slf4j
@Component
public class ScheduledController {
@Autowired
ScheduledServiceImpl scheduledService;

@Scheduled(cron = "0 0/2 * * * ?")
public void pushDataScheduled(){
    log.info("start push data scheduled!");
    scheduledService.pushData();
    log.info("end push data scheduled!");
}
 


并行方式

当定时任务很多的时候,为了提高任务执行效率,可以采用并行方式执行定时任务,任务之间互不影响, 
只要实现SchedulingConfigurer接口就可以。

/**
    定时任务并行执行
**/
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
    scheduledTaskRegistrar.setScheduler(setTaskExecutors());
}

@Bean(destroyMethod="shutdown")
public Executor setTaskExecutors(){
    return Executors.newScheduledThreadPool(3); // 3个线程来处理。
}}
 


5.cron表达式
常用: 秒、分、时、日、月、年

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 
0 0 12 * * ? 每天中午12点触发 
0 0/5 0 * * ? 每5分钟执行一次

具体更多可以参考: https://www.cnblogs.com/linjiqin/archive/2013/07/08/3178452.html

6.深入理解,使用延迟队列代替定时任务
在并行执行的时候,创建线程池采用了newScheduledThreadPool这个线程池。 Executors框架中存在几种线程池的创建,一种是 newCachedThreadPool() ,一种是 newFixedThreadPool(), 一种是 newSingleThreadExecutor()

其中newScheduledThreadPool() 线程池的采用的队列是延迟队列。newScheduledThreadPool() 线程池的特性是定时任务能够定时或者周期性的执行任务。

源码: 
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

其中线程池核心线程数是自己设定的,最大线程数是最大值。阻塞队列是自定义的延迟队列:DelayedWorkQueue()
 

<think>我们讨论的是Spring Boot中的@Scheduled注解。这个注解用于定时任务的执行。在Spring Boot中,默认情况下,@Scheduled任务是在一个单线程线程池中执行的,这意味着任务是同步执行的,一个任务执行完毕后,下一个任务才会开始执行。 具体来说,Spring Boot通过`TaskScheduler`接口来处理定时任务。默认情况下,如果没有提供自定义的`TaskScheduler` bean,Spring Boot使用一个`ThreadPoolTaskScheduler`,但是注意,这个默认的任务调度器实际上只有一个线程(即大小为1的线程池)。因此,所有的@Scheduled任务都在这个唯一的线程上运行,导致任务是同步执行的(按顺序执行,不会并发执行多个任务)。 这可能导致一个问题:如果一个任务执行时间很长,那么后续的任务可能会被延迟执行,因为它们必须等待前一个任务完成。 为了避免这种情况,我们可以配置一个具有多个线程线程池,这样任务就可以并发执行。 下面是一个配置线程定时任务执行器的例子: 我们可以通过创建一个`TaskScheduler` bean来覆盖默认的配置,例如: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @Configuration @EnableScheduling public class SchedulerConfig { @Bean public ThreadPoolTaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); // 设置线程池大小 scheduler.setThreadNamePrefix("scheduled-task-"); // 线程名前缀 scheduler.initialize(); return scheduler; } } ``` 这样,当我们有多个定时任务时,它们就可以在多个线程中并发执行。 另外,我们也可以在某个定时任务方法上使用`@Async`注解,使得这个任务在调用时会被提交到一个异步线程池中执行,这样就不会阻塞定时任务线程池中的线程。但是注意,这需要同时启用异步支持(使用`@EnableAsync`)。 总结: 1. 默认情况下,@Scheduled任务使用线程线程池(同步执行,一个接一个)。 2. 可以通过配置自己的`TaskScheduler` bean(如设置线程池大小)来支持并发执行。 3. 也可以结合`@Async`实现异步执行,但这需要配置异步线程池(通过`@EnableAsync`和`@Async`)。 下面是一个简单的示例,展示如何定义定时任务: ```java import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class ScheduledTasks { // 每5秒执行一次 @Scheduled(fixedRate = 5000) public void task1() { System.out.println("Task1 executed by thread: " + Thread.currentThread().getName()); } @Scheduled(fixedDelay = 3000) public void task2() { System.out.println("Task2 executed by thread: " + Thread.currentThread().getName()); } } ``` 在默认配置下,这两个任务会共用同一个线程(名为“pool-1-thread-1”或类似的)。如果我们配置了上面的`TaskScheduler`,那么它们可能会被不同的线程执行。 现在,我们来回答你的问题: - @Scheduled默认使用线程是单线程的(同步执行,一个任务执行完再执行下一个)。 - 通过配置线程线程池,可以实现异步执行(多个任务并发执行)。 注意:即使配置了多线程,每个定时任务在自己的线程中运行,但同一个定时任务(同一个方法)的多次执行,默认是不会并发执行的。也就是说,如果上一次执行还没有结束,即使到了下一次执行时间,也不会启动新的执行。这是因为Spring默认使用`fixedDelay`和`fixedRate`的语义是等待上一次执行完成再计算下一次执行(对于fixedDelay)或者在上一次开始后间隔固定时间执行(fixedRate,但如果上一次没执行完,会等待,所以实际上还是顺序的)。但是,如果配置了多个线程,那么不同的任务可以并发执行,而同一个任务如果配置了`fixedRate`并且执行时间超过间隔,那么可能会在新的线程中执行(因为线程池有多个线程,但是同一个任务的两个执行实例可能被不同的线程执行,但默认同一个任务不会并发执行,因为Spring使用一个锁来防止同一个任务并发执行)。要允许同一个任务并发执行,可以添加`@Scheduled`的`concurrent`属性,但Spring默认不支持,通常我们可以使用`@Async`来实现。 实际上,Spring@Scheduled默认不支持同一个任务的并发执行(即同一个任务方法在上一次执行未完成时,即使到了下一次执行时间,也不会启动新的执行)。如果想要支持并发,可以结合`@Async`注解。 例如: ```java @Async @Scheduled(fixedRate = 5000) public void task1() { // 异步执行,即使前一个任务没有完成,也会在新的线程上启动 } ``` 同时,在主配置类上添加`@EnableAsync`注解。 因此,我们可以得出: - 默认:同步(单线程执行所有定时任务,同一个任务不会并发执行,不同任务按顺序执行) - 配置线程任务调度器:不同任务可以并发执行,同一个任务仍然不会并发执行(因为Spring对同一个任务加了锁) - 使用`@Async`:可以实现同一个任务的并发执行(因为每次执行都在独立的线程上,且不受定时任务调度的锁限制)。 关于同一个任务并发执行的锁,可以参考`ScheduledAnnotationBeanPostProcessor`中的`ScheduledTaskRegistrar`,它默认使用`Runnable`包装任务,并在执行时加锁(通过一个`ConcurrentHashMap`存储的`Lock`,每个任务一个锁)。所以同一个任务不会并发执行。而不同任务之间没有互斥,所以如果线程池有多个线程,不同任务可以同时执行。 最后,我们回答你的问题: - 默认线程:单线程线程名为“pool-1-thread-1”或类似的) - 同步还是异步:默认是同步执行(任务排队执行,不会并发) 但请注意,这里的“同步”是指任务按顺序执行(一个接一个),而不是指使用synchronized关键字。而异步则是指多个任务可以同时执行。 所以,如果你希望任务异步执行(并发),那么你需要配置线程池(对于不同任务并发)或者使用@Async(对于同一个任务也允许并发)。 现在,按照要求,最后提出相关问题: §§相关问题§§: 1. 在Spring Boot中,如何配置多个定时任务并发执行? 2. 如何让同一个定时任务方法在上一次执行未完成时,下一次执行可以并发执行? 3. 默认的定时任务线程配置是什么?如何自定义线程池? 4. @Scheduled注解的cron表达式和fixedRate、fixedDelay有什么区别? </think>在Spring Boot中,`@Scheduled`注解定时任务执行机制如下: ### 线程模型 1. **默认线程池** 任务由**单线程任务调度器**执行,核心线程数为**1**(通过`ThreadPoolTaskScheduler`实现)。线程名为`task-1`(或类似格式)。 2. **同步执行** - 所有定时任务**共享同一个线程**。 - 任务默认**串行执行**:一个任务完成后才会执行下一个任务(即使任务间隔时间已到)。 - 示例输出: ```log Task1 executed by thread: task-1 Task2 executed by thread: task-1 // 同一线程 ``` ### 原因分析 Spring Boot自动配置类`TaskSchedulingAutoConfiguration`创建了一个默认调度器: ```java @Bean @ConditionalOnMissingBean public TaskScheduler taskScheduler(TaskSchedulerBuilder builder) { return builder.build(); } ``` 默认配置为: ```java poolSize = 1 // 核心线程数=1 threadNamePrefix = "task-" // 线程名前缀 ``` ### 异步执行解决方案 #### 方案1:配置线程调度器(推荐) ```java import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar; @Configuration public class SchedulerConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); // 设置线程池大小 scheduler.setThreadNamePrefix("custom-scheduler-"); scheduler.initialize(); taskRegistrar.setTaskScheduler(scheduler); } } ``` 此时任务会分配不同线程: ```log Task1 executed by thread: custom-scheduler-1 Task2 executed by thread: custom-scheduler-2 // 不同线程并行执行 ``` #### 方案2:结合`@Async`异步注解 ```java import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.Scheduled; @EnableAsync // 启用异步支持 @Configuration public class AsyncConfig {} @Component public class ScheduledTasks { @Async // 每次任务独立线程执行 @Scheduled(fixedRate = 1000) public void asyncTask() { System.out.println("Async task: " + Thread.currentThread().getName()); } } ``` 输出示例: ```log Async task: task-1 Async task: task-2 // 不同线程 ``` ### 关键结论 | 特性 | 默认行为 | 优化后行为 | |--------------------|------------------------|--------------------------| | **执行线程** | 单线程(task-1) | 多线程/线程池 | | **任务并发性** | 串行执行(同步) | 并行执行(异步) | | **阻塞风险** | 一个任务阻塞会延迟后续 | 任务间独立不受影响 | | **线程资源** | 固定1线程 | 可配置线程池大小 |
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值