为什么你的 Spring Task 定时任务没有定时执行?

前言

定时任务的使用,在开发中可谓是家常便饭了,定时发送邮件、短信。 避免数据库,数据表过大,定时将数据转储。通知、对账等等。

当然实现定时任务的方式也有很多,比如使用 linux 下的 contab 脚本,jdk 中自带的 Timer 类。Spring Task 或是 Quartz 。

相信你也有过如下的疑问:

  • Spring Task 的 contab 的表达式 和 linux 下的 contab 有什么区别?
  • crontab 表达式记不住?
  • 定时任务阻塞会有什么影响?
  • 多个定时任务的情况下是如何运行的?
  • 具有相同表达式的定时任务,他们的执行顺序如何?
  • 为什么async异步任务没有生效?

所以这篇文章,我们来介绍一下,在 Spring Task 中, 定时任务的执行原理及相关问题。演示环境为 Spring Boot 项目。

SpringBoot 定时任务的原理

相信绝大部分开发者都使用过Spring Boot 为我们提供的定时任务的 Starter 和定时任务的注解。所以我们来主要介绍一下 Spring Boot 实现定时任务的原理,和其相关注解的作用。

Spring 在 3.0版本后通过 @Scheduled 注解来完成对定时任务的支持。

为什么你的 Spring Task 定时任务没有定时执行?

在我们使用时,需要在Application 启动类上加上 @EnableScheduling 注解,它是从Spring 3.1后开始提供的。

为什么你的 Spring Task 定时任务没有定时执行?

由于现在 Spring3 版本较低,使用得比较少了,可能并不会考虑太多细节,大多只需要关注目标实现,所以我们在配套使用两个注解的时候,并不会出现什么问题。

在3.0 中 ,是通过

<code>  

<executor>

<scheduler>

<annotation-driven> executor="executor" proxy-target-class="true" />
/<annotation-driven>/<code>

上述的 XML 配置 和 @Scheduled 配合实现定时任务的,而我们这里的 @EnableScheduling 其实类似的和它等价,是用来发现注解了 @Scheduled 的方法,没有这个注解光有 @Scheduled 是无法执行的,大家可以做一个简单案例测试一下,其底层是 Spring 自己实现的一套定时任务的处理逻辑,所以使用起来比较简单。

任务一直阻塞会怎么样?

介绍了两个注解的作用后,我们来开始做实验,简单的写一个定时执行的方法。

为什么你的 Spring Task 定时任务没有定时执行?

每隔 20s 输出一句话,在输出几行记录后,打上了一个断点。

对后续的任务有什么影响呢?

为什么你的 Spring Task 定时任务没有定时执行?

可以看到,断点时的后续任务是阻塞着的,从图上,我们还可以看出初始化的名为pool-1-thread-1 的线程池同样证实了我们的想法,线程池中只有一个线程,创建方法是:

<code>Executors.newSingleThreadScheduledExecutor();
/<code>

从这个例子来看,断点时,任务会一直阻塞,当阻塞恢复后,会立马执行阻塞的任务。线程池内部时采用 DelayQueue 延迟队列实现的,它的特点是: 无界、延迟、阻塞的一种队列,能按一定的顺序对工作队列中的元素进行排列。

为什么你的 Spring Task 定时任务没有定时执行?

多个定时任务的执行

通过上面的实验,我们知道,咋看默认情况下,任务的线程池,只会有一个线程来执行任务,如果有多个定时任务,它们也应该是串行执行的。

为什么你的 Spring Task 定时任务没有定时执行?

从上图可以看出,一旦线程执行任务1后,就会睡眠2分钟。线程在死循环内部一直处于Running 状态。

为什么你的 Spring Task 定时任务没有定时执行?

通过观察日志,根本没有任务2的输出,我们知道默认情况下,多个定时任务是串行执行的,类似于多辆车过单行道的桥,如果一个任务出现阻塞,其他的任务都会受到影响。

那如果线程池包含多个线程的情况下,多个定时任务并发的情况是什么样?

为什么你的 Spring Task 定时任务没有定时执行?

串行当然很好理解,就是上文说的汽车过桥,依次通过。再来理解并发,区别于并行,并发是指一个处理器同时处理多个任务,而并行是指多个(核)处理器同时处理多个不同的任务。并发不一定同一时间发生,而并行,指的是同一时间。

具有相同表达式的定时任务,他们的执行顺序如何?

从上面的实验同样能知道,具有相同表达式的定时任务,还是和调度有关,如果是默认的线程池,那么会串行执行,首先获取到cpu时间片的先执行。在多线程情况下,具体的先后执行顺序和线程池线程数和所用线程池所用队列等等因素有关。

Spring Task和linux crontab的cron语法区别?

两者的 cron 表达式其实很相似,需要注意的是 linux 的contab 只为我们提供了最小颗粒度为分钟级的任务,而java中最小的粒度是从秒开始的。具体细节如下图:

为什么你的 Spring Task 定时任务没有定时执行?

在cron语法中容易犯的错误

以spring 中的task为例,cron 表达式中 "/" 代表每的意思,“*/10”表示每10个单位。

在cron 语法 中很多人会犯错误。比如要求写出每十分钟定时执行的 cron 语句,可能会有以下版本的出现:

为什么你的 Spring Task 定时任务没有定时执行?

所以当我们写完cron 表达式的时候,可以适当的调低执行间隔时间来测试,或是通过一些在线的网站来检测你的cron脚本是否正确。

@Async异步注解原理及作用

Spring task中 和异步相关的注解有两个 , 一个是 @EnableAsync ,另一个就是 @Async 。

为什么你的 Spring Task 定时任务没有定时执行?

首先我们单纯的在方法上引入 @Async 异步注解,并且打印当前线程的名称,实验后发现,方法仍然是由一个线程来同步执行的。

和@schedule 类似 还是通过@Enable开头的注解来控制执行的。我们在启动类上加入@EnableAsync 后再观察输出内容。

为什么你的 Spring Task 定时任务没有定时执行?

可以发现,默认情况下,其内部是使用的名为SimpleAsyncTaskExecutor的线程池来执行任务,而且每一次任务调度,都会新建一个线程。

使用@EnableAsync注解开启了Spring的异步功能,Spring会按照如下的方式查找相应的线程池用于执行异步方法: 查找实现了TaskExecutor接口的Bean实例。

如果上面没有找到,则查找名称为taskExecutor并且实现了Executor接口的Bean实例。

如果还是没有找到,则使用SimpleAsyncTaskExecutor,该实现每次都会创建一个新的线程执行任务。

并发执行任务如何配置?

方式一,我们可以将默认的线程池替换为我们自定义的线程池。通过ScheduleConfig配置文件实现SchedulingConfigurer接口,并重写setSchedulerfang方法。

可实现AsyncConfigurer接口复写getAsyncExecutor获取异步执行器,getAsyncUncaughtExceptionHandler获取异步未捕获异常处理器

<code>@Configurationpublic
class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
/<code>

方式二:不改变任务调度器默认使用的线程池,而是把当前任务交给一个异步线程池去执行。

<code>  @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;
}
/<code>

其他问题

如果是定时任务没有生效,需要检查 @EnableScheduling 注解是否加上。 如果是异步没有生效,需要检查 @EnableAsync 注解是否加上,并且定义线程池,否则仍然是串行执行的。

总结

文章介绍了SpringBoot 定时任务的原理, 3.0版本前后的区别,通过单线程任务阻塞实验,探究了延迟队列及串行、并行、并发的概念。对比了linux下的 contab 和spring的cron表达式区别以及常犯的错误。最后通过实验异步注解,两种方式配置线程池,让任务高效运作,希望本文能让你有所收获。

创建了一个java方面的互助群,和其他传统的学习群不同。

本群主要致力于解决项目中的疑难问题,在遇到项目难以解决的

在本群,你可以

1)阐述你在开发过程中遇到的问题,群友集思广益,高效解答。

2)分享自己学习的一些心得,让后来学习者少踩坑。

3)资源共享,无论是好的学习视频还是文档都可以在群内共享。

别人有可能可以给你提供一些思路和看法

同样,如果你也乐于帮助别人,那解决别人遇到的问题,也同样对你是一种锻炼。

想邀你加入这个有温度的社群

分享经验和心得

集思广益,高效解答问题

帮助他人,锻炼自己


分享到:


相關文章: