相关推荐recommended
Spring Task 超详解版
作者:mmseoamin日期:2023-12-11

目录

一、定时任务的理解

二、入门案例

三、Cron表达式

四、Cron实战案例

五、多线程案例


一、定时任务的理解

定时任务即系统在特定时间执行一段代码,它的场景应用非常广泛:

  • 购买游戏的月卡会员后,系统每天给会员发放游戏资源。
  • 管理系统定时生成报表。
  • 定时清理系统垃圾。

    定时任务的实现主要有以下几种方式:

    1. Java自带的java.util.Timer类,这个类允许调度一个java.util.TimerTask任务。使用这种方式可以让程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
    2. Quartz。这是一个功能比较强大的的调度器,可以让程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。
    3. Spring3.0以后自带Spring Task,可以将它看成一个轻量级的Quartz,使用起来比 Quartz简单许多,在课程中我们使用Spring Task实现定时任务

    二、入门案例

    创建SpringBoot项目,在启动类开启定时任务。

    也就是在启动类上方添加@EnableScheduling注解即可开启定时任务,代码如下:

    package com.example.springboottaskdemo;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    @SpringBootApplication
    @EnableScheduling
    public class SpringboottaskdemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringboottaskdemoApplication.class, args);
        }
    }
    

    编写定时任务类 

    @Component
    public class MyTask {
      // 定时任务方法,每秒执行一次
      @Scheduled(cron="* * * * * *")
      public void task1() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println(sdf.format(new Date()));
     }
    }

    启动项目,定时任务方法按照配置定时执行。 

    Spring Task 超详解版,第1张

    OK,果然如此,每隔一秒输出当前时间

    @Scheduled写在方法上方,指定该方法定时执行。常用参数如下:

    1. cron:cron表达式,定义方法执行的时间规则。
    2. fixedDelay:任务立即执行,之后每隔多久执行一次,单位是毫秒,上一次任务结束后计算下次执行的时间。

    OK,先来一个案例,代码如下:任务结束后每隔五秒执行一次

    // 立即执行,任务结束后五秒执行一次
        @Scheduled(fixedDelay = 5000)
        public void task1() throws InterruptedException {
            SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
            System.out.println("Task1: "+sdf.format(new Date()));
        }

    效果如下: 

    Spring Task 超详解版,第2张

    OK,果然如此,注意这个是任务结束后每隔五秒,如果方法中间加了一个sleep方法,那么执行时间还要加上sleep里面的值,比如说中间加了一个sleep(1000),那么就会每隔6秒执行一次。

    fixedRate:任务立即执行,之后每隔多久执行一次,单位是毫秒,上一次任务开始后计算下次执行的时间。

    案例如下,代码如下:

    // 立即执行,之后每五秒执行一次
        @Scheduled(fixedRate = 5000)
        public void task2() throws InterruptedException {
            SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
            // 没有影响五秒输出一次
            Thread.sleep(1000);
            System.out.println("Task2: "+sdf.format(new Date()));
        }

     OK,看如下执行代码确实是不受到sleep影响的

    Spring Task 超详解版,第3张

    initialDelay:项目启动后不马上执行定时器,根据initialDelay的值延时执行。 为了突出刚刚说的fixedDelay会受到sleep影响,这里配合fixedDelay来结合测试演示一下:

    代码如下:

    // 项目启动后三秒执行,之后每六秒执行一次
        @Scheduled(fixedDelay = 5000,initialDelay = 3000)
        public void task3() throws InterruptedException {
            SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
            // 没有影响五秒输出一次
            Thread.sleep(1000);
            System.out.println("Task3: "+sdf.format(new Date()));
        }

    OK,看运行结果也是隔了三秒才出现第一次打印时间,并且打印时间是隔六秒打印一次 

    Spring Task 超详解版,第4张

    三、Cron表达式

    Spring Task依靠Cron表达式配置定时规则。Cron表达式是一个字符串,分为6或7个域,每一个域代表一个含义,以空格隔开。有如下两种语法格式:

    1. Seconds Minutes Hours DayofMonth Month DayofWeek Year
    2. Seconds Minutes Hours DayofMonth Month DayofWeek

    Seconds(秒):域中可出现 , - * / 四个字符,以及0-59的整数

    • * :表示匹配该域的任意值,在Seconds域使用 * ,表示每秒钟都会触发
    • , :表示列出枚举值。在Seconds域使用 5,20 ,表示在5秒和20秒各触发一次。
    • - :表示范围。在Seconds域使用 5-20 ,表示从5秒到20秒每秒触发一次
    • / :表示起始时间开始触发,然后每隔固定时间触发一次。在Seconds域使用 5/20 , 表示5秒触发一次,25秒,45秒分别触发一次。

      Minutes(分):域中可出现 , - * / 四个字符,以及0-59的整数

      Hours(时):域中可出现 , - * / 四个字符,以及0-23的整数

      DayofMonth(日期):域中可出现 , - * / ? L W C 八个字符,以及1-31的整数

      • C :表示和当前日期相关联。在DayofMonth域使用 5C ,表示在5日后的那一天触发,且每月的那天都会触发。比如当前是10号,那么每月15号都会触发。
      • L :表示最后,在DayofMonth域使用 L ,表示每个月的最后一天触发。
      • W :表示工作日,在DayofMonth域用 15W ,表示最接近这个月第15天的工作日触发,如果15号是周六,则在14号即周五触发;如果15号是周日,则在16号即周一触发;如果15号是周二则在当天触发。

        注:

        1. 该用法只会在当前月计算,不会到下月触发。比如在DayofMonth域用 31W ,31号是周日,那么会在29号触发而不是下月1号。
        2. 在DayofMonth域用 LW ,表示这个月的最后一个工作日触发。

        Month(月份):域中可出现 , - * / 四个字符,以及1-12的整数或JAN-DEC的单词缩写

        DayofWeek(星期):可出现 , - * / ? L # C 八个字符,以及1-7的整数或SUN-SAT 单词缩写,1代表星期天,7代表星期六

        • C :在DayofWeek域使用 2C ,表示在2日后的那一天触发,且每周的那天都会触发。比如当前是周一,那么每周三都会触发。
        • L :在DayofWeek域使用 L ,表示在一周的最后一天即星期六触发。在DayofWeek域使用 5L ,表示在一个月的最后一个星期四触发。
        • # :用来指定具体的周数, # 前面代表星期几, # 后面代表一个月的第几周,比如 5#3 表示一个月第三周的星期四。
        • ? :在无法确定是具体哪一天时使用,用于DayofMonth和DayofWeek域。例如在每月的20日零点触发1次,此时无法确定20日是星期几,写法如下: 0 0 0 20 * ? ;或者在每月的最后一个周日触发,此时无法确定该日期是几号,写法如下: 0 0 0 ? * 1L

          Year(年份):域中可出现 , - * / 四个字符,以及1970~2099的整数。该域可以省略,表示每年都触发。

          四、Cron实战案例

          下面有常用的案例,大家可以参考一下

          Cron实战案例
          含义表达式
          每隔5分钟触发一次0 0/5 * * * *
          每小时触发一次0 0 * * * *
          每天的7点30分触发0 30 7 * * *
          周一到周五的早上6点30分触发0 30 6 ? * 2-6
          每月最后一天早上10点触发0 0 10 L * ?
          每月最后一个工作日的18点30分触发0 30 18 LW * ?
          2030年8月每个星期六和星期日早上10点触发0 0 10 ? 8 1,7 2030
          每天10点、12点、14点触发0 0 10,12,14 * * *
          朝九晚五工作时间内每半小时触发一次0 0 0/30 9-17 ? * 2-6
          每周三中午12点触发一次0 0 12 ? * 4
          每天12点触发一次0 0 12 * * *
          每天14点到14:59每分钟触发一次0 * 14 * * *
          每天14点到14:59每5分钟触发一次0 0/5 14 * * *
          每天14点到14:05每分钟触发一次0 0-5 14 * * *
          每月15日上午10:15触发0 15 10 15 * ?
          每月最后一天的上午10:15触发0 15 10 L * ?
          每月的第三个星期五上午10:15触发0 15 10 ? * 6#3

          好啦,通过这些大家应该就可以领悟了

          五、多线程案例

          Spring Task定时器默认是单线程的,如果项目中使用多个定时器,使用一个线程会造成效率低下。

          比如说我们设置了两个定时任务,那么因为Spring Task是单线程,如果在第一个定时任务加了一个sleep方法,那么会等第一个方法响应后在执行第二个任务,就很浪费cpu运行时间。代码如下:

              @Scheduled(cron = "* * * * * *")
              public void task1() throws InterruptedException {
                  System.out.println(Thread.currentThread().getId()+"线程执行任务1 - "+new SimpleDateFormat("HH:mm:ss").format(new Date());
                  Thread.sleep(5000);
              }
              @Scheduled(cron = "* * * * * *")
              public void task2() throws InterruptedException {
                  System.out.println(Thread.currentThread().getId()+"线程执行任务2 - "+new SimpleDateFormat("HH:mm:ss").format(new Date());
              }

          执行效果如下:可以看到是先执行了任务2,但是他们都要隔五秒才能运行一次,因为通过线程号可以知道这是同一个线程。

          Spring Task 超详解版,第5张

          因此任务1较浪费时间,会阻塞任务2的运行。此时我们可以给SpringTask配置线程池。代码如下:

          package com.example.springboottaskdemo;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.scheduling.annotation.SchedulingConfigurer;
          import org.springframework.scheduling.config.ScheduledTaskRegistrar;
          import java.util.concurrent.Executors;
          @Configuration
          public class SchedulingConfig implements SchedulingConfigurer {
              @Override
              public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
                  // 创建线程池,设置五个线程
                  taskRegistrar.setScheduler(Executors.newScheduledThreadPool(4));
              }
          }
          

          这样就不会出现阻塞问题了,因为两个任务不是同一个线程,接下来我们再次运行看看:

          Spring Task 超详解版,第6张

          执行效果如上,确实不会影响到任务2的运行,但是如果定时任务过多,超过了配置的线程池的线程数量还是会运行错乱。

          Ok,SpringBoot到这里就完结撒花了。