SpringBoot 定时任务@Scheduled之单线程多线程问题

文章介绍了SpringBoot中@Scheduled注解的单线程特性,导致定时任务执行顺序依赖。给出了三种解决方案:扩大线程池、使用多线程Scheduled或异步执行方法,以确保任务并行执行和避免阻塞。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  springboot框架中提供了@Scheduled注解,让我们快速实现一个定时任务。但其实这个注解是单线程的,多个定时任务之间是需要一个一个来执行的。如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。

测试

@Component
public class ScheduledTimerComp {

    private Logger logger = LoggerFactory.getLogger(ScheduledTimerComp.class);

    @Scheduled(cron = "0/2 * * * * ?")
    public void test1() {
        String id = UUID.randomUUID().toString().replace("-", "");
        logger.info(String.format("[%s]test1...开启", id));
        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.info(String.format("[%s]tes1....结束", id));
    }

    @Scheduled(cron = "0/2 * * * * ?")
    public void test2() {
        String id = UUID.randomUUID().toString().replace("-", "");
        logger.info(String.format("[%s]test2...开启", id));
        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.info(String.format("[%s]test2...结束", id));
    }

}

 执行完后:

发现test2被阻塞了,只有test1执行完后,test2才会执行。

原因

定时任务的配置中默认设定了一个SingleThreadScheduledExecutor。源码中,从ScheduledAnnotationBeanPostProcessor类往下找,在定时任务注册类(ScheduledTaskRegistrar)中的ScheduleTasks中看到这样的一段判断:

if (this.taskScheduler == null) {
    this.localExecutor = Executors.newSingleThreadScheduledExecutor();
    this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}

也就是说如果taskScheduler为空,那么就给定时任务做了一个单线程的线程池。

解决

同时在这个定时任务注册类(ScheduledTaskRegistrar)中,看到了还有一个设置taskScheduler的方法,那么也就是说,我们是可以去set它的

public void setScheduler(Object scheduler) {
    Assert.notNull(scheduler, "Scheduler object must not be null");
    if (scheduler instanceof TaskScheduler) {
        this.taskScheduler = (TaskScheduler) scheduler;
    }
    else if (scheduler instanceof ScheduledExecutorService) {
        this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));
    }
    else {
        throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
    }
}

方案一、

 扩大原定时任务线程池中的核心线程数(推荐)

 新建一个类,实现SchedulingConfigurer接口

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
    }
}

这个方案,在工程启动后,会默认启动50个线程,放在线程池中。每个定时任务会占用1个线程。注:相同的定时任务,执行的时候,还是在同一个线程中。
例如,程序启动,每个定时任务占用一个线程。test1开始执行,test2也开始执行,彼此之间异步不相关。如果test1卡死了,那么下个周期,test1还是处理卡死状态,test2可以正常执行。也就是说,test1某一次卡死了,不会影响其他线程,但是他自己本身这个定时任务会一直等待上一次任务执行完成!

方案二、

Scheduled配置成成多线程执行

新建一个类,工程启动时加载

@Configuration
@EnableAsync
public class ScheduleConfig {
 
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(50);
        return taskScheduler;
    }
}
    @Async
    @Scheduled(cron = "0/2 * * * * ?")
    public void test1() {
        String id = UUID.randomUUID().toString().replace("-", "");
        logger.info(String.format("[%s]test1...开启", id));
        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.info(String.format("[%s]tes1....结束", id));
    }

    @Async
    @Scheduled(cron = "0/2 * * * * ?")
    public void test2() {
        String id = UUID.randomUUID().toString().replace("-", "");
        logger.info(String.format("[%s]test2...开启", id));
        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.info(String.format("[%s]test2...结束", id));
    }

这个方案,每次定时任务启动的时候,都会创建一个单独的线程来处理。也就是说同一个定时任务也会启动多个线程处理。
例如:test1和test2一起处理,但是test1卡死了,test2是可以正常执行的。且下个周期,test1还是会正常执行,不会因为上一次卡死了,影响test1。
但是test1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。
这时候,可能需要对工程进行重启了

方案三、

将所有@Scheduled注释的方法内部改成线程异步执行

private ExecutorService service = Executors.newFixedThreadPool(5);

    @Scheduled(cron = "0/2 * * * * ?")
    public void test1() {
        service.execute(()->{
            String id = UUID.randomUUID().toString().replace("-", "");
            logger.info(String.format("[%s]test1...开启", id));
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            logger.info(String.format("[%s]tes1....结束", id));
        });
        
    }

    @Scheduled(cron = "0/2 * * * * ?")
    public void test2() {
        service.execute(()->{
            String id = UUID.randomUUID().toString().replace("-", "");
            logger.info(String.format("[%s]test2...开启", id));
            try {
                Thread.sleep(2 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            logger.info(String.format("[%s]test2...结束", id));
        });
    }

### 如何在 Spring Boot 中使用 `@Scheduled` 注解实现定时任务 #### 配置主类或配置类 为了启用定时任务功能,在 Spring Boot 的主应用程序类或者某个配置类上需要添加 `@EnableScheduling` 注解。这一步是必不可少的,因为该注解会激活 Spring 容器内的调度机制[^2]。 ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling // 启用定时任务功能 public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` --- #### 创建定时任务方法 创建一个普通的组件类(通过 `@Component` 或其他类似的注解标记),并在其中定义带有 `@Scheduled` 注解的方法。此注解用于指定任务执行的时间间隔或其他触发条件[^3]。 以下是几种常见的定时任务设置方式: 1. **基于固定时间间隔的任务** 可以使用 `fixedRate` 属性来设定每次任务之间的最小间隔时间(单位为毫秒)。 ```java import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class ScheduledTasks { @Scheduled(fixedRate = 5000) // 每隔 5 秒运行一次 public void fixedRateTask() { System.out.println("Fixed Rate Task executed at: " + System.currentTimeMillis()); } } ``` 2. **基于延迟时间间隔的任务** 如果希望当前任务完成后等待一段时间再启动下一个任务,则可以使用 `fixedDelay` 属性。 ```java @Scheduled(fixedDelay = 10000) // 当前任务结束后等待 10 秒再运行下一次 public void fixedDelayTask() { System.out.println("Fixed Delay Task executed at: " + System.currentTimeMillis()); } ``` 3. **基于 Cron 表达式的复杂任务计划** 对于更复杂的场景,比如每天凌晨两点执行某项操作,可以通过 `cron` 属性提供自定义表达式。 ```java @Scheduled(cron = "0 0/5 * * * ?") // 每五分钟运行一次 public void cronTask() { System.out.println("Cron Task executed at: " + System.currentTimeMillis()); } @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨两点运行一次 public void dailyCronTask() { System.out.println("Daily Cron Task executed at: " + System.currentTimeMillis()); } ``` --- #### 注意事项 - 默认情况下,`@Scheduled` 基于单线程池工作。如果多个任务可能重叠或耗时较长,建议调整线程池大小以提高并发能力。 - 若要修改默认行为,可以在配置文件中加入如下属性: ```properties spring.task.scheduling.pool.size=5 # 设置线程池大小为 5 ``` - 时间计算均基于 JVM 所在服务器的本地时区。因此需注意部署环境与时区的一致性问题。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值