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)); }); } ``` ### 推荐方案 一、三 组合使用。