使用ScheduledExecutorService线程池手动动态控制定时任务

背景

在日常开发过程中,使用定时任务去执行一些业务逻辑是很常见的一种场景。比如定时发送短信,邮件,电商系统的定时自动收货、定时上下架功能等等。

一般实现定时任务有以下几种方案:

JDK自带 

  • JDK自带的Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,一般用的较少。
  • JDK1.5+ 新增的ScheduledExecutorService:是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。

第三方框架 

  使用 Quartz、elastic-job、xxl-job 等开源第三方定时任务框架,适合分布式项目应用。不过配置起来稍显复杂,不太易上手。
Spring Task 

  Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。使用 Spring 提供的一个注解 @Schedule即可,开发简单,使用比较方便。

本文主要向大家介绍使用注解和ScheduledExecutorService来实现定时任务。

使用

注解实现

正式开始之前,我们先看一下通过注解实现定时任务的方法。

1.启动类添加 @EnableScheduling 注解

2.在被spring管理的类的方法上添加 @Scheduled 注解

    @Scheduled(cron = "0/1 * * * * *")
    public void task01() throws InterruptedException {
        log.info("task01,当前时间{},线程名称{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(10);
    }

    @Scheduled(cron = "0/1 * * * * *")
    public void task02() {
        log.info("task02,当前时间{},线程名称{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                Thread.currentThread().getName());
    }

    @Scheduled(cron = "0/1 * * * * *")
    public void task03() {
        log.info("task03,当前时间{},线程名称{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                Thread.currentThread().getName());
    }

启动项目之后,控制台输出以下内容:

2021-07-28 16:32:25.020  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:32:25,线程名称scheduling-1
2021-07-28 16:32:25.021  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:32:25,线程名称scheduling-1
2021-07-28 16:32:25.021  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task01,当前时间2021-07-28 16:32:25,线程名称scheduling-1
2021-07-28 16:32:35.025  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:32:35,线程名称scheduling-1
2021-07-28 16:32:35.026  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:32:35,线程名称scheduling-1
2021-07-28 16:32:36.001  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:32:36,线程名称scheduling-1
2021-07-28 16:32:36.002  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task01,当前时间2021-07-28 16:32:36,线程名称scheduling-1
2021-07-28 16:32:46.004  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:32:46,线程名称scheduling-1
2021-07-28 16:32:46.005  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:32:46,线程名称scheduling-1
2021-07-28 16:32:47.001  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:32:47,线程名称scheduling-1
2021-07-28 16:32:47.002  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:32:47,线程名称scheduling-1
2021-07-28 16:32:47.003  INFO 27964 --- [   scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task01,当前时间2021-07-28 16:32:47,线程名称scheduling-1

可以看出,在这种情况下,所有的定时任务都是在同一个线程(线程名称scheduling-1)下执行的,并且只有在前一个定时任务执行完毕之后才会执行其他的定时任务。

这时,如果我们配置一个类型为 TaskScheduler 类型的bean,然后重启程序,可看到这时控制台的内容是这样的

配置Bean

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        //设置线程池大小
        threadPoolTaskScheduler.setPoolSize(10);
        //设置线程名称前缀
        threadPoolTaskScheduler.setThreadNamePrefix("schedule-task");
        return threadPoolTaskScheduler;
    }

控制台

2021-07-28 16:38:38.028  INFO 24956 --- [ schedule-task3] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:38:38,线程名称schedule-task3
2021-07-28 16:38:38.028  INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:38,线程名称schedule-task2
2021-07-28 16:38:38.028  INFO 24956 --- [ schedule-task1] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task01,当前时间2021-07-28 16:38:38,线程名称schedule-task1
2021-07-28 16:38:39.002  INFO 24956 --- [ schedule-task6] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:39,线程名称schedule-task6
2021-07-28 16:38:39.002  INFO 24956 --- [ schedule-task5] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:38:39,线程名称schedule-task5
2021-07-28 16:38:40.002  INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:38:40,线程名称schedule-task2
2021-07-28 16:38:40.002  INFO 24956 --- [ schedule-task3] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:40,线程名称schedule-task3
2021-07-28 16:38:41.001  INFO 24956 --- [ schedule-task6] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:41,线程名称schedule-task6
2021-07-28 16:38:41.001  INFO 24956 --- [ schedule-task4] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:38:41,线程名称schedule-task4
2021-07-28 16:38:42.001  INFO 24956 --- [ schedule-task5] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:42,线程名称schedule-task5
2021-07-28 16:38:42.001  INFO 24956 --- [schedule-task10] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task02,当前时间2021-07-28 16:38:42,线程名称schedule-task10
2021-07-28 16:38:43.001  INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl  : task03,当前时间2021-07-28 16:38:43,线程名称schedule-task2

通过结果我们可以看出,这时定时任务是多线程执行的,不同任务也不用等待其他任务执行完毕之后才能执行。

其实这个类就是我们下面要介绍的通过 ThreadPoolTaskScheduler 线程池来控制定时任务。下面我们来看一下详细的用法。

线程池实现

1.启动类添加 @EnableScheduling 注解

2.创建定时任务类

ScheduleController.java

/** 
 * <p>
 *  接口控制定时任务开始和停止
 * </p>
 *
 * @className ScheduleController
 * @author Sue
 * @date 2021/7/28
 **/
@RestController
@RequestMapping("/task")
public class ScheduleController {

    ScheduleTaskService scheduleTaskService;

    public ScheduleController(ScheduleTaskService scheduleTaskService) {
        this.scheduleTaskService = scheduleTaskService;
    }

    @PostMapping("/startCron")
    public R startCron() {
        scheduleTaskService.startCorn();
        return R.ok("定时任务启动成功!");
    }

    @PostMapping("/stopCron")
    public R stopCron() {
        scheduleTaskService.stopCorn();
        return R.ok("定时任务关闭成功!");
    }
}

ScheduleTaskService.java

/**
 * <p>
 *  定时任务
 * </p>
 *
 * @className ThreadPoolTaskSchedulerService
 * @author Sue
 * @create 2021/7/21 
 **/
public interface ScheduleTaskService {
    /**
     * <p>
     *  开始定时任务
     * </p>
     *
     * @return boolean
     * @author Sue
     * @date 2021/7/21
     */
    boolean startCorn();

    /**
     * <p>
     *  关闭定时任务
     * </p>
     *
     * @return boolean
     * @author Sue
     * @date 2021/7/21
     */
    boolean stopCorn();
}

ScheduleTaskServiceImpl.java

/**
 * <p>
 *  定时任务
 * </p>
 *
 * @className ThreadPoolTaskSchedulerServiceImpl
 * @author Sue
 * @create 2021/7/21 
 **/
@Slf4j
@Service
public class ScheduleTaskServiceImpl implements ScheduleTaskService {

    private ScheduledFuture<?> future;

    ThreadPoolTaskScheduler threadPoolTaskScheduler;

    public ScheduleTaskServiceImpl(ThreadPoolTaskScheduler threadPoolTaskScheduler) {
        this.threadPoolTaskScheduler = threadPoolTaskScheduler;
    }

    @Override
    public boolean startCorn() {
        if (future != null) {
            future.cancel(true);
            log.info("定时任务已停止");
        }
        //每10秒执行一次,一般情况下这里可以通过读取外部配置来实现动态的修改定时任务的执行时间
        String cornConfig = "0/10 * * * * *";
        future = threadPoolTaskScheduler.schedule(new ScheduledTaskRunnable(), new CronTrigger(cornConfig));
        log.info("定时任务开启");
        return false;
    }

    @Override
    public boolean stopCorn() {
        if (future != null) {
            future.cancel(true);
            log.info("定时任务已停止");
        }
        return false;
    }

    static class ScheduledTaskRunnable implements Runnable {
        @Override
        public void run() {
            //需要执行的业务逻辑
            log.info("定时任务开始执行,每10秒执行一次,当前时间{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
    }
}

3.项目启动后,调用接口,控制定时任务的启动和停止

测试

调用startCorn接口,可以看出定时任务成功开启

 调用stopCorn接口,停止定时任务

补充

corn表达式的使用

cron 表达式是一个字符串,该字符串由 6 个空格分为 7 个域,每一个域代表一个时间含义。 通常定义 “年” 的部分可以省略,实际常用的 Cron 表达式由前 6 部分组成。格式如下:

[秒] [分] [时] [日] [月] [周] [年]
Seconds  Minutes  Hours   Day-of-Month  Month   Day-of-Week    Year (optional field)

 需要说明的是,Cron 表达式中,“周” 是从周日开始计算的。“周” 域上的 1 表示的是周日,7 表示周六。

通配符说明

  • * 表示所有值. 例如:在分的字段上设置 “*”,表示每一分钟都会触发
  • ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
  • - 表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发。
  • , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
  • / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次。
  • L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
  • W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W"前只能设置具体的数字,不允许区间”-").
  • # 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;

提示:
'L’和 'W’可以组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发;
周字段的设置,若使用英文字母是不区分大小写的,即MON 与mon相同;

例子

  • "0 */1 * * * ?" 每隔 1 分钟执行一次
  • "0 24,30 * * * ?" 在24分,30分执行一次
  • "0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点
  • "0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时
  • "0 0 12 ? * WED" 表示每个星期三中午12点
  • "0 0 12 * * ?" 每天中午12点触发
  • "0 15 10 ? * *" 每天上午10:15触发
  • "0 15 10 * * ?" 每天上午10:15触发
  • "0 15 10 * * ? *" 每天上午10:15触发
  • "0 15 10 * * ? 2005" 2005年的每天上午10:15触发
  • "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
  • "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
  • "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
  • "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
  • "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
  • "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
  • "0 15 10 15 * ?" 每月15日上午10:15触发
  • "0 15 10 L * ?" 每月最后一日的上午10:15触发
  • "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
  • "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
  • "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

 

posted @ 2021-07-28 15:48  少说点话  阅读(2635)  评论(0编辑  收藏  举报
网站运行: