spring 定时任务Scheduler和异步任务Async

2023-12-20 17:38:12

1. 概述

Spring框架分别通过TaskExecutor和TaskScheduler接口为任务提供异步执行和调度。

ThreadPoolTaskScheduler(继承自TaskScheduler)ThreadPoolTaskExecutor(继承自TaskExecutor)备注
含义任务调度器,定时任务线程池执行器,异步任务
用途主要用于在指定的时间间隔内执行任务或定时任务用于执行异步任务和多线程任务
线程池基于java.util.concurrent.ScheduledExecutorService基于 java.util.concurrent.ThreadPoolExecutorthreadPool
默认配置springboot默认线程池大小为1springboot默认线程池大小8
注解@EnableScheduling
@Scheduled
@EnableAsync
@EnableScheduling
@Async
@Scheduled
1注解的方法必须是public方法


2.方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。


3. 异步方法使用注解@Async的返回值只能为void或者Future
函数返回值函数可以有返回值函数没有返回值
异步执行单个任务同步执行。



当执行时间大于我们间隔时间时,上一次任务执行完成后才会开始下一个任务。
非阻塞异步执行。


每次执行定时任务都会新开一个线程,即使之前的任务没有完成。

2. 定时任务Scheduler

定时任务会创建线程池ScheduledThreadPoolExecutor,用于执行任务。springboot默认Scheduler线程池corePoolSize=1

2.1 定时任务 - 相关注解及使用方法(一个简单的例子)

spring定时任务使用非常简单,只需要添加两个注解@EnableScheduling,@Scheduled

1.@EnableScheduling:在spring管理的类上添加都可以,通常添加在启动类上

@EnableScheduling
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
       SpringApplication.run(Application.class, args);    }
}

2. @Scheduled: 在定时任务的实现方法上添加该注解(@Scheduled注解的详细介绍见4.?@Scheduled参数详解

@Slf4j
@Service
public class ScheduledTask {

    @Scheduled(cron ="* * * * * *")
    public int ScheduledTaskOne() throws InterruptedException {
        log.info("------- ScheduledTaskOne start...");
        Thread.sleep(10000L);
        log.info("------- ScheduledTaskOne done.");
        return 1;
    }
}

如上任务配置了每秒运行一次。由于函数中执行了 Thread.sleep(10000L),可以知道该任务执行完成大约10秒。

启动服务,可以看到如下log:

2023-10-17 11:38:00.012  INFO 18620 --- [   scheduling-1] com.shirley.spring.tasks.ScheduledTask   : ------- ScheduledTaskOne start...
2023-10-17 11:38:10.024  INFO 18620 --- [   scheduling-1] com.shirley.spring.tasks.ScheduledTask   : ------- ScheduledTaskOne done.
2023-10-17 11:38:11.002  INFO 18620 --- [   scheduling-1] com.shirley.spring.tasks.ScheduledTask   : ------- ScheduledTaskOne start...
2023-10-17 11:38:21.004  INFO 18620 --- [   scheduling-1] com.shirley.spring.tasks.ScheduledTask   : ------- ScheduledTaskOne done.
2023-10-17 11:38:22.003  INFO 18620 --- [   scheduling-1] com.shirley.spring.tasks.ScheduledTask   : ------- ScheduledTaskOne start...

从log中记录的时间可以看出,虽然@Scheduled配置了每秒启动一次,但第二次执行ScheduledTaskOne还是会等到第一次执行完毕后才运行,而不是根据cron配置每秒运行一次。这和@Async的行为是不同的!

2.2 定时任务 - 修改配置

2.2.1 查看默认配置

spring提供了一些配置项对scheduler进行配置。这里介绍一个查看spring配置项的方法。

1. 通过IDEL(如Intellij IDEA)找到依赖项中的spring-boot-autoconfigure:***,这个就是springboot自动配置模块。

2. 在org.springframework.boot.autoconfigure.task package下,有两个*Properties类,这两个类就是配置项对应的Bean。TaskSchedulingProperties为Scheduler的配置,TaskExecutionProperties为异步任务的相关配置。

3. 查看TaskExecutionProperties源码(部分代码如下),可以看出scheduler相关配置以spring.task.scheduling 开头。springboot会通过类中各属性的set方法给属性赋值。例如,配置项spring.task.scheduling.threadNamePrefix 会通过TaskSchedulingProperties.setThreadNamePrefix(String threadNamePrefix) 函数复制给threadNamePrefix

// 配置项前缀
@ConfigurationProperties("spring.task.scheduling")
public class TaskSchedulingProperties {

    private final Pool pool = new Pool();
    private final Shutdown shutdown = new Shutdown();

     // 线程名称前缀
    private String threadNamePrefix = "scheduling-";

    public static class Pool {
        // 默认线程池大小为1
        private int size = 1;

    }

    public static class Shutdown {
         // 当线程池调用shutdown时,是否等待任务完成
        private boolean awaitTermination;
         // 等待任务完成最长时间。
        private Duration awaitTerminationPeriod;
    }
}

2.2.2 修改配置

根据TaskSchedulingProperties类,我们可以得到任务调度相关配置项有哪些。如下是一份完整的配置(如下配置文件为yaml格式,如果配置文件为.properties, 请自行将yaml转为properties。)

spring:
  task:
    scheduling:
      # 线程名前缀  
      threadNamePrefix: shirley-scheduler-
      pool:
        # 线程池大小。corePoolSize
        size: 5
      shutdown:
        # 当线程池shutdown时,是否等待已分配的任务执行完成
        awaitTermination: true
        # shutdown后,等待任务完成
的最长时间
        awaitTerminationPeriod: "2s"

在日常开发中,通常我们只需要配置spring.task.scheduling.pool.size

3. 线程池执行器(异步任务) Async

定时任务会创建线程池用于执行任务。springboot 默认Async线程池 corePoolSize=8

3.1 异步任务 - 相关注解及使用方法(一个简单的例子)

1. 添加注解@EnableAsync,@EnableScheduling启动异步任务

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

2. 在定时任务的实现方法上添加该注解@Async,@Scheduled

@Slf4j
@Service
public class AsyncTask {
    @Async
    @Scheduled(cron = "* * * * * *")
    public void AsyncTaskOne() throws InterruptedException {
        log.info("------- AsyncTaskOne start...");
        Thread.sleep(10000L);
        log.info("------- AsyncTaskOne done.");
    }
}

如上任务配置了每秒运行一次。由于函数中执行了 Thread.sleep(10000L),可以知道该任务执行完成大约10秒。

启动服务,可以看到如下log

2023-Oct-18 12:34:07.022 INFO  [task-1] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:08.001 INFO  [task-2] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:09.013 INFO  [task-3] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:10.008 INFO  [task-4] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:11.008 INFO  [task-5] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:12.005 INFO  [task-6] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:13.003 INFO  [task-7] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:14.016 INFO  [task-8] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...
2023-Oct-18 12:34:17.035 INFO  [task-1] c.s.s.t.AsyncTask:16 - ------- AsyncTaskOne done.
2023-Oct-18 12:34:17.037 INFO  [task-1] c.s.s.t.AsyncTask:14 - ------- AsyncTaskOne start...

从log中可以看出,同时有8个相同的任务同时执行,线程分别为[task-1] ~ [task8]。

3.2 异步任务 - 修改配置

通过查看spring-boot-autoconfigure:***.jar 下类 org.springframework.boot.autoconfigure.task.TaskExecutionProperties,可以得知异步任务相关配置项。配置项说明如下:

spring:
  task:
    execution:
      # 线程名前缀  
      threadNamePrefix: shirleyTask-
     
      # 线程池相关配置
      pool:
        # 对应线程池的corePoolSize
        coreSize: 8
        # 对应线程池 workQueue的大小。
        queueCapacity: 100
        # 对应线程池的maxPoolSize
        maxSize: 100
        # 对应线程池 keepAliveTime,TimeUnit.SECONDS
        keepAlive: 60s
        # 线程池coreThread空闲时间超过keepAlive后,是否terminated。
        allowCoreThreadTimeout: true
      
      shutdown:
        # 当线程池shutdown时,是否等待已分配的任务执行完成
        awaitTermination: false
        # shutdown后,等待任务完成
的最长时间
        awaitTerminationPeriod: "10s"

3.3 自定义线程池

如果不想用spring默认的线程池,比如希望更改默认线程池拒绝策略(AbortPolicy),也可以自定义线程池。

自定义线程池只需要两步:

1. 定义一个TaskExecutor类型的spring bean。如下例子中,定义了名为"shirleyExecutor" 的TaskExecutor

    @Bean("shirleyExecutor")
    public Executor shirleyExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大线程数
        executor.setMaxPoolSize(5);
        //核心线程数
        executor.setCorePoolSize(5);
        //任务队列的大小
        executor.setQueueCapacity(10);
        //线程前缀名
        executor.setThreadNamePrefix("shirley-exe-");
        //线程存活时间
        executor.setKeepAliveSeconds(10);

        /**
         * 拒绝处理策略
         * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
         * AbortPolicy():直接抛出异常。
         * DiscardPolicy():直接丢弃。
         * DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

2. 在@Async注解上指定自定义的Bean

    @Async("shirleyExecutor")
    @Scheduled(cron = "* * * * * *")
    public void shirleyExeTask() throws InterruptedException {
        log.info("------- shirleyExeTask start...");
        Thread.sleep(10000L);
        log.info("------- shirleyExeTask done.");
    }

注意:Async执行器方法只能返回void或Further

运行后,可以从log看出已经使用了自定义的Executor。在这个例子中,threadName已经变成了shirley-exe-*

2023-Oct-18 17:21:39.014 INFO  [shirley-exe-1] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...
2023-Oct-18 17:21:40.011 INFO  [shirley-exe-2] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...
2023-Oct-18 17:21:41.010 INFO  [shirley-exe-3] c.s.s.t.AsyncTask:38 - ------- shirleyExeTask start...

4. @Scheduled参数详解

spring 异步任务Async和定时任务Scheduler都可以通过@Scheduled注解进行调度。@Scheduled注解有三种方式配置调度频率:

  • cron: cron表达式,和Linux的cron表达式类似。
  • fixedRate:每隔fixedRate执行一次任务。如@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)表示每隔5秒执行一次任务。
  • fixedDelay:上一次运行结束和下一次运行开始之间的时间间隔。如 @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) 表示上一次运行结束后5秒,开始下一次执行

注意,如果通过cron,fixedRate配置执行频率,Scheduler和Async的行为不同:

  • 在定时任务Scheduler模式下,如果任务执行时间超过fixedRate,则会在上一个任务执行结束后才开始下一次执行;
  • 在Async定时任务模式下,即使上一次任务还没有结束,

4.1 cron说明

cron表达式一共6位,分别表示:秒,分,时,日期,月,星期

 ┌───────────── second (0-59)
 │ ┌───────────── minute (0 - 59)
 │ │ ┌───────────── hour (0 - 23)
 │ │ │ ┌───────────── day of the month (1 - 31)
 │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
 │ │ │ │ │ ┌───────────── day of the week (0 - 7)
 │ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
 │ │ │ │ │ │
 * * * * * *
 
* 第一位,表示秒,取值0-59
* 第二位,表示分,取值0-59
* 第三位,表示小时,取值0-23
* 第四位,日期天/日,取值1-31
* 第五位,日期月份,可以为:1-12;也可以表示为英文月份缩写:JAN~DEC
* 第六位,星期,可以为:0-7(0或7表示星期天); 也可以为英文星期缩写:MON~SUN

表达式说明:

(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年...

(?)问号:问号只能出现在日期和星期这两个位置,表示这个位置的值不确定,每天3点执行,所以第六位星期的位置,我们是不需要关注的,就是不确定的值。同时:日期和星期是两个相互排斥的元素,通过问号来表明不指定值。比如,1月10日,比如是星期1,如果在星期的位置是另指定星期二,就前后冲突矛盾了。

(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12

(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四

(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60    另:*/y,等同于0/y

英文月份/星期缩写:可以用月份/星期 英文单词的前三个字母表示月份和星期几,大小写不敏感。

(L)最后一个:日期和星期如果包含L字母,表示最后一天。如日期使用“L”表示月的最后一天;“L-n”表示 n号到月末。

(LW)最后一个工作日(周一到周五,不是中国的法定工作日)

(d#n)第n个星期d

一些有用的cron表达式:

0 0 3 * * ?             每天3点执行
0 5 3 * * ?             每天3点5分执行
0 5 3 ? * *             每天3点5分执行,与上面作用相同
0 5/10 3 * * ?          每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行
0 10 3 ? * 1            每周星期天,3点10分 执行,注:1表示星期天
0 10 3 ? * 1#3          每个月的第三个星期一执行,#号只能出现在星期的位置
0 0 9-17 * * MON-FRI    每月周一到周五9点到17点
0 0 0 25 12 ?           每个圣诞节(12月25日)0点
0 0 0 L * *             每个月最后一天0点
0 0 0 L-3 * *           每月3号到月底的0点
0 0 0 1W * *            每月第一个工作日的0点
0 0 0 LW * *            每月最后一个工作日的0点
0 0 0 * * 5L            每个月最后一个周五的0点
0 0 0 * * THUL          每月最后一个星期四0点
0 0 0 ? * 5#2           每月第二个星期五0点
0 0 0 ? * MON#1         每月第一个星期一0点

4.1.1 cron宏

由于0 0 * * * * 的可读性比较差,spring定义了一些宏,替代常见的cron表达式,比如:@Scheduled(cron = "@hourly").spring支持的宏如下表:

含义
@yearly 或 @annually每年一次 (0 0 0 1 1 *)
@monthly每月一次(0 0 0 1 * *)
@weekly每周一次 (0 0 0 * * 0)
@daily (or @midnight)每天一次 (0 0 0 * * *)
@hourly每小时一次 (0 0 * * * *)

示例代码:

    // 每小时运行一次
    @Scheduled(cron = "@hourly")
    public void shirleyExeTask() throws InterruptedException {
        log.info("------- shirleyExeTask start...");
        Thread.sleep(10000L);
        log.info("------- shirleyExeTask done.");

文章来源:https://blog.csdn.net/zhangjunli/article/details/135108202
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。