SpringBoot 定时任务 @Scheduled 之单线程多线程问题
2024-08-08 17:17:24
SpringBoot 框架中提供了 @Scheduled 注解,让我们快速实现一个定时任务。但其实这个注解是单线程的,多个定时任务之间是需要一个一个来执行的。如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。
```java
@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]test1....结束", 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 才会执行。
## 解决方案
### 一、**扩大原定时任务线程池中的核心线程数(推荐)**
新建一个类,实现SchedulingConfigurer接口
```java
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(20));
}
}
```
这个方案,在工程启动后,会默认启动 20 个线程,放在线程池中。每个定时任务会占用 1 个线程。
注:相同的定时任务,执行的时候,还是在一个线程中。
例如,程序启动,每个定时任务占用一个线程。test1 开始执行,test2 也开始执行,彼此之间异步不相关。如果 test1 卡死了,那么下个周期,test1 还是处理卡死状态,test2 可以正常执行。也就是说,test1 某一次卡死了,不会影响其他线程,但是他自己本身这个定时任务会一直等待上一次任务执行完成!
### 二、Scheduled配置成异步多线程执行 (不推荐)
```java
@Configuration
@EnableAsync
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(50);
return taskScheduler;
}
}
```
```java
@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]test1....结束", 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 中的卡死线程越来越多,会导致线程池占满,还是会影响到定时任务。
这时候,可能需要对工程进行重启了。
### 三、**将所有 @Scheduled 注释的方法内部改成线程池异步执行 (推荐)**
```java
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5, // corePoolSize 核心线程池大小
10, // maximumPoolSize 线程池最大数量
60, // keepAliveTime 空闲存活的时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(20), // 任务队列(阻塞队列)
new ThreadPoolExecutor.CallerRunsPolicy()); // 饱和策略
@Scheduled(cron = "0/2 * * * * ?")
public void test1() {
poolExecutor.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]test1....结束", id));
});
}
@Scheduled(cron = "0/2 * * * * ?")
public void test2() {
poolExecutor.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));
});
}
```
### 推荐方案 一、三 组合使用。