在日常开发中,定时任务的应用已经变得十分常见,例如定时发送邮件、短信,数据定时转储等。然而,有时候我们会遇到定时任务没有按照预期执行的情况。本文将讨论 Spring Task 中定时任务执行的原理以及可能遇到的相关问题。
SpringBoot 定时任务的原理
绝大部分开发者在使用 Spring Boot 时都会使用它提供的定时任务 Starter 和定时任务的注解。因此,让我们首先了解一下 Spring Boot 实现定时任务的原理以及相关注解的作用。
自 Spring 3.0 版本以后,Spring 使用 @Scheduled 注解来支持定时任务。
在使用时,我们需要在应用的启动类上加上 @EnableScheduling 注解,该注解从 Spring 3.1 开始提供。
然而,在 Spring 3.0 版本之前,我们通常会通过 XML 配置和 @Scheduled 注解配合使用来实现定时任务。而 @EnableScheduling 注解实际上相当于该 XML 配置的简化版。它的作用是发现标记了 @Scheduled 注解的方法,没有该注解的方法将不会被执行。但是在大多数情况下,我们只需要关注目标实现,而不必深究其背后的细节。
任务一直阻塞会怎么样?
为了更好地理解定时任务的执行过程,我们进行了一个简单的实验:编写一个每隔 20 秒执行一次的定时任务,并在其中加入一个断点。
实验结果显示,当遇到断点时,后续的任务会被阻塞。这是因为线程池中只有一个线程用于执行任务,而该线程被阻塞后,后续任务将无法执行。线程池内部采用 DelayQueue 延迟队列实现,其特点是无界、延迟、阻塞,能够按照一定的顺序执行工作队列中的元素。
多个定时任务的执行
根据前面的实验结果,我们知道默认情况下,任务的线程池只有一个线程,因此多个定时任务应该是串行执行的。如果一个任务被阻塞,其他任务也会受到影响。
那么,如果我们将线程池的线程数设置为多个,会发生什么呢?实验结果显示,即使线程池中包含多个线程,多个定时任务仍然是串行执行的。只有在使用自定义的线程池,并且在任务上加上 @Async 注解时,才能实现并发执行。
具有相同表达式的定时任务,它们的执行顺序如何?
具有相同表达式的定时任务的执行顺序与调度有关。默认情况下,它们是串行执行的,首先获取到 CPU 时间片的任务会先执行。在多线程的情况下,具体的执行顺序取决于线程池的线程数、所用队列等因素。
Spring Task 和 Linux crontab 的 cron 语法区别?
两者的 cron 表达式非常相似,但需要注意的是,Linux 的 crontab 只提供了分钟级别的最小粒度,而 Spring Task 的最小粒度从秒开始。
在 cron 语法中容易犯的错误
在编写 cron 表达式时,很容易犯一些错误。例如,要求编写每十分钟执行一次的 cron 语句时,可能会写成类似以下的版本:
0 0/10 * * * ?
因此,在编写 cron 表达式后,建议适当降低执行间隔时间进行测试,或者使用在线工具检测 cron 脚本是否正确。
@Async 异步注解原理及作用
Spring Task 中与异步相关的注解有两个,一个是 @EnableAsync,另一个是 @Async。
默认情况下,@Async 注解会使方法仍然由一个线程同步执行。要想实现异步执行,需要在启动类上加上 @EnableAsync 注解,并定义一个线程池。
并发执行任务如何配置?
我们可以通过两种方式来配置并发执行任务:
- 将默认的线程池替换为自定义的线程池。
- 不改变任务调度器默认使用的线程池,而是将任务交给一个异步线程池去执行。
通过这些方式,我们可以让任务高效地并发执行,提高系统的性能。
方式一,我们可以将默认的线程池替换为我们自定义的线程池。通过ScheduleConfig配置文件实现SchedulingConfigurer接口,并重写setSchedulerfang方法。 可实现AsyncConfigurer接口复写getAsyncExecutor获取异步执行器,getAsyncUncaughtExceptionHandler获取异步未捕获异常处理器 @Configurationpublic class ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } } 方式二:不改变任务调度器默认使用的线程池,而是把当前任务交给一个异步线程池去执行。 @Scheduled(fixedRate = 1000*10,initialDelay = 1000*20) @Async("hyqThreadPoolTaskExecutor") public void test(){ System.out.println(Thread.currentThread().getName()+"--->xxxxx--->"+Thread.currentThread().getId()); } //自定义线程池 @Bean(name = "hyqThreadPoolTaskExecutor") public TaskExecutor getMyThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("hyq-threadPool-"); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor;
其他问题
- 如果定时任务没有生效,需要检查是否加上了 @EnableScheduling 注解。
- 如果异步任务没有生效,需要检查是否加上了 @EnableAsync 注解,并且定义了线程池,否则仍然是串行执行的。
总结
本文介绍了 Spring Boot 定时任务的原理,通过实验探究了延迟队列及串行、并行、并发的概念。对比了 Linux 下的 crontab 和 Spring 的 cron 表达式区别以及常见的错误。最后通过实验异步注解,两种方式配置线程池,使任务能够高效运行。希望本文能够对你有所帮助。