手记

springboot之一文带你搞懂Scheduler定时器(修订-详尽版)

本篇文章是SpringBoot项目实战(6):开启定时任务一文的修订版。

在开发程序中总是避免不了一些周期性定时任务,比如定时同步数据库、定时发送邮件、定时初始化数据等等。

那么在springboot中如何使用定时任务呢? 本文将由浅入深逐渐揭开定时任务的“神秘面纱”。

本文知识点:

  1. springboot中如何使用Scheduler
  2. springboot中如何动态修改Scheduler的执行时间(cron)
  3. springboot中如何实现多线程并行任务

想了解springboot中的Scheduler?看懂本文妥妥的了~!

如何使用Scheduler?

  1. 使用@EnableScheduling启用定时任务
  2. 使用@Scheduled编写相关定时任务

开启定时任务

在程序中添加@EnableScheduling注解即可启用Spring的定时任务功能,这类似于Spring的XML中的<task:*>功能。

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

关于@Scheduled

通过查看Scheduled注解类的源码可知该注解支持如下几种方式使用定时任务

  1. cron()
  2. fixedDelay()
  3. fixedDelayString()
  4. fixedRate()
  5. fixedRateString()
  6. initialDelay()
  7. initialDelayString()

注:由于xx和xxString方法只是参数类型不同用法一致,所以本文对xxString表达式方法不做解释。感兴趣的同学可以去查阅下ScheduledAnnotationBeanPostProcessor类的processScheduled方法。

cron

这个也是定时任务中最常用的一种调度方式,主要在于它的功能强大,使用方便。
支持cron语法,与linux中的crontab表达式类似,不过Java中的cron支持到了秒。
表达式规则如下:

{秒} {分} {时} {日} {月} {周} {年(可选)}

这儿有很多朋友记不住,或者和crontab的表达式记混。

Tips:linux的crontab表达式为:{分} {时} {日} {月} {周}

其实这儿也没有很好的记忆方式,采用最简单的方式:死记硬背即可(( ╯╰ ))。

cron各选项的取值范围及解释:

取值范围 null 备注
0-59 参考①
0-59 参考①
0-23 参考①
1-31 参考②
1-12或JAN-DEC 参考①
1-7或SUN-SAT 参考③
1970-2099 参考①

参考①

“*” 每隔1单位时间触发;
"," 在指定单位时间触发,比如"10,20"代表10单位时间、20单位时间时触发任务
"-" 在指定的范围内触发,比如"5-30"代表从5单位时间到30单位时间时每隔1单位时间触发一次
"/" 触发步进(step),"/“前面的值代表初始值(”“等同"0”),后面的值代表偏移量,比如"0/25"或者"/25"代表从0单位时间开始,每隔25单位时间触发1次;"10-45/20"代表在[10-45]单位时间内每隔20单位时间触发一次

参考②

“* , - /” 这四个用法同①
"?" 与{周}互斥,即意味着若明确指定{周}触发,则表示{日}无意义,以免引起 冲突和混乱;
“L” 如果{日}占位符如果是"L",即意味着当月的最后一天触发
"W "意味着在本月内离当天最近的工作日触发,所谓最近工作日,即当天到工作日的前后最短距离,如果当天即为工作日,则距离为0;所谓本月内的说法,就是不能跨月取到最近工作日,即使前/后月份的最后一天/第一天确实满足最近工作日;因此,"LW"则意味着本月的最后一个工作日触发,"W"强烈依赖{月}
“C” 根据日历触发,由于使用较少,暂时不做解释

参考③

“* , - /” 这四个用法同①
"?" 与{日}互斥,即意味着若明确指定{日}触发,则表示{周}无意义,以免引起冲突和混乱
"L" 如果{周}占位符如果是"L",即意味着星期的的最后一天触发,即星期六触发,L= 7或者 L = SAT,因此,“5L"意味着一个月的最后一个星期四触发
”#" 用来指定具体的周数,"#“前面代表星期,”#"后面代表本月第几周,比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四,因此,“5L"这种形式只不过是”#“的特殊形式而已
"C” 根据日历触发,由于使用较少,暂时不做解释

其他关于cron的详细介绍请参考Spring Task 中cron表达式整理记录

/**
 * 在11月7号晚上22点的7分到8分之间每隔半分钟(30秒)执行一次任务
 *
 * @author zhangyd
 */
@Scheduled(cron = "0/30 7-8 22 7 11 ? ")
public void doJobByCron() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByCron start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(5));
	System.out.println(String.format("[%s] %s doJobByCron end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:00.004
[1] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:07:01.004
[2] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:30.001
[2] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:07:30.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:00.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:08:01.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:30.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron end   @ 22:08:33.002

给大家安利一款在线生成Cron表达式的神器:http://cron.qqe2.com。使用简单,一看就会。

fixedDelay

上一次任务执行完成后,延时固定长度时间执行下一次任务。关键词:上一次任务执行完成后
就如官方文档中所说:在最后一次调用结束到下一次调用开始之间以毫秒为单位进行等待,等待完成后执行下一次任务

Execute the annotated method with a fixed period in milliseconds between the end of the last invocation and the start of the next.

通过代码可以更好的理解

private AtomicInteger integer = new AtomicInteger(0);
/**
 * 上次任务执行完的3秒后再次执行
 *
 * @author zhangyd
 */
@Scheduled(fixedDelay = 3000)
public void doJobByFixedDelay() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByFixedDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(10));
	System.out.println(String.format("[%s] %s doJobByFixedDelay end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看控制台输出

[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:03.235
[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:06.236
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:09.238
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:10.238
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:13.240
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end   @ 18:28:21.240

可以看到不管每个任务实际需要执行多长时间,该任务再次执行的时机永远都是在上一次任务执行完后,延时固定长度时间后执行下一次任务。

fixedRate

官方文档中已经很明白的说明了此方法的用法。

Execute the annotated method with a fixed period in milliseconds between invocations.

按照固定的速率执行任务,无论之前的任务是否执行完毕。关键词:不管前面的任务是否执行完毕

private AtomicInteger integer = new AtomicInteger(0);
/**
 * 固定每3秒执行一次
 *
 * @author zhangyd
 */
@Scheduled(fixedRate = 3000)
public void doJobByFixedRate() throws InterruptedException {
	int index = integer.incrementAndGet();
	System.out.println(String.format("[%s] %s doJobByFixedRate start @ %s", index, Thread.currentThread(), LocalTime.now()));
	// 这儿随机睡几秒,方便查看执行效果
	TimeUnit.SECONDS.sleep(new Random().nextInt(10));
	System.out.println(String.format("[%s] %s doJobByFixedRate end   @ %s", index, Thread.currentThread(), LocalTime.now()));
}

查看控制台打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:23.520
[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:24.521
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:26.515
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:31.516
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:32.515
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate end   @ 18:57:47.519

这儿需要重点关注每个任务的起始时间下一次任务的起始时间。如果说我们不看这个结果,直接就代码来说,我们会认为,程序每隔3秒执行一次,第二次任务和第一次任务的间隔时间是3秒,没毛病。但是我们通过上面打印的日志发现,程序并不是按照我们预想的流程执行的。

注:为方便阅读,任务命名为T+任务的编号,比如T1表示第一个任务,以此类推。

从上面结果看,T1起始时间到T2起始时间之间间隔了3秒,T2起始时间到T3起始时间间隔了5秒,T3起始时间到T4起始时间间隔了1秒。

为什么会有这种现象呢?这正如上面所说:“按照固定的延迟时间执行任务,无论之前的任务是否执行完毕

那么按照这种规则,咱们预先估计下每次任务的执行时间就能解释上面的问题。T1起始时间是23秒,T2起始时间是26,以此类推,T3理论上是29,T4是32,T5是35,T6是38,T7是41…

这种机制也可以称为“任务编排”,也就是说从fixedRate任务开始的那一刻,它后续的任务执行的时间已经被预先编排好了。如果定时任务执行的时间 大于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后立即执行;如果定时任务执行的时间 小于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后等到预编排的时间时执行。

initialDelay

该条指令主要是用于配合fixedRatefixedDelay使用的,作用是在fixedRatefixedDelay任务第一次执行之前要延迟的毫秒数,说白了:它的作用就是在程序启动后,并不会立即执行该定时任务,它将在延迟一段时间后才会执行该定时任务

Number of milliseconds to delay before the first execution of a fixedRate() or fixedDelay() task.

本例为了方便测试,需要首先知道这个定时任务的bean在什么时候装载完成,或者说这个定时任务的bean什么时候初始化完成,所以,需要处理一下代码。

@Component
public class AppSchedulingConfigurer implements ApplicationListener<ContextRefreshedEvent> {

    private AtomicInteger integer = new AtomicInteger(0);


    /**
     * 第一次延迟5秒后执行,之后按fixedRate的规则每3秒执行一次
     *
     * @author zhangyd
     */
    @Scheduled(initialDelay = 5000, fixedRate = 3000)
    public void doJobByInitialDelay() throws InterruptedException {
        int index = integer.incrementAndGet();
        System.out.println(String.format("[%s] %s doJobByInitialDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
        // 这儿随机睡几秒,方便查看执行效果
        TimeUnit.SECONDS.sleep(new Random().nextInt(5));
        System.out.println(String.format("[%s] %s doJobByInitialDelay end   @ %s", index, Thread.currentThread(), LocalTime.now()));
    }

    /**
     * Spring容器初始化完成
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        System.out.println(String.format("springboot上下文context准备完毕时. %s start @ %s", Thread.currentThread(), LocalTime.now()));
    }
}

如上,使用Spring的事件监听应用启动的时间,以此打印出initialDelay应该从什么时候开始等待。

(关于Spring和Springboot的事件,本文不做深入介绍,后期会专文讲解这一块内容)

查看打印结果

springboot上下文context准备完毕时. Thread[main,5,main] start @ 21:18:42.092
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:47.023
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:18:57.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:59.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end   @ 21:19:00.025

由此可以明显看出,initialDelay确实是在bean上下文准备完毕(容器已初始化完成)时延迟了5秒钟后执行的fixedRate任务。

如何动态修改执行时间(cron)?

实现动态配置cron的好处就是可以随时修改任务的执行表达式而不用重启服务。

想实现这种功能,我们只需要实现SchedulingConfigurer接口重写configureTasks接口即可。

@Component
public class DynamicScheduledConfigurer implements SchedulingConfigurer {

    // 默认每秒执行一次定时任务
    private String cron = "0/1 * * * * ?";
    private AtomicInteger integer = new AtomicInteger(0);

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
            int index = integer.incrementAndGet();
            System.out.println(String.format("[%s] %s 动态定时任务 start @ %s", index, Thread.currentThread(), LocalTime.now()));
        }, triggerContext -> {
            CronTrigger trigger = new CronTrigger(this.getCron());
            return trigger.nextExecutionTime(triggerContext);
        });
    }

    public String getCron() {
        return cron;
    }

    public void setCron(String cron) {
        System.out.println(String.format("%s Cron已修改!修改前:%s,修改后:%s @ %s", Thread.currentThread(), this.cron, cron, LocalTime.now()));
        this.cron = cron;
    }
}

例子中默认的cron表示每秒执行一次定时任务,我们要做的就是动态修改这个执行时间。

接下来编写一个controller负责处理cron以及预编排5组该cron将要执行的时间节点

@SpringBootApplication
@Controller
@EnableScheduling
public class ScheduleApplaction {

    @Autowired
    DynamicScheduledConfigurer dynamicScheduledConfigurer;

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

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    /**
     * 修改动态定时任务的cron值
     */
    @RequestMapping("/updateTask")
    @ResponseBody
    public void updateTask(String cron) {
        dynamicScheduledConfigurer.setCron(cron);
    }

    /**
     * 预解析5次该cron将要执行的时间节点
     *
     * @param cron 带解析的cron
     * @return
     * @throws IOException
     */
    @RequestMapping("/parseCron")
    @ResponseBody
    public List<String> parseCron(String cron) throws IOException {
        String urlNameString = "http://cron.qqe2.com/CalcRunTime.ashx?CronExpression=" + URLEncoder.encode(cron, "UTF-8");
        URL realUrl = new URL(urlNameString);
        URLConnection connection = realUrl.openConnection();
        connection.setRequestProperty("accept", "*/*");
        connection.setRequestProperty("connection", "Keep-Alive");
        connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
        connection.connect();

        StringBuilder result = new StringBuilder();
        try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return JSONArray.parseArray(result.toString(), String.class);
    }
}

html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>动态修改cron</title>
</head>
<body>

<div>
    <input type="text" name="cron" id="cron" value="0/2 * * * * ?" title="cron">
    <button type="button" id="submitBtn">提交</button>

    <dl id="parserBox">
        <dt>最近5次运行的时间</dt>
    </dl>
</div>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script>
    $(function () {
        $("#submitBtn").click(function () {
            var cron = $("#cron").val();
            $.post("/updateTask", {cron: cron});
            $.getJSON("/parseCron?cron=" + encodeURIComponent(cron), function (rs) {
                var html = '';
                $.each(rs, function (i, v) {
                    html += '<dd>' + v + '</dd>';
                });
                var $parserBox = $("#parserBox");
                $parserBox.children("dd").remove();
                $parserBox.append(html);
            })
        });
    })
</script>
</body>
</html>

启动项目后先查看控制台输出,默认应该是每秒执行一次定时任务。

[1] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:23.010
[2] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:24.001
[3] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:25.001
[4] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:26.002

接下来修改一下默认cron,如下图,将定时任务修改为每5秒执行一次

[56] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:18.002
[57] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:19.001
Thread[http-nio-8080-exec-7,5,main] Cron已修改!修改前:0/1 * * * * ?,修改后:0/5 * * * * ? @ 14:01:19.963
[58] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:20.001
[59] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:25.002
[60] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:30.002
[61] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:35.002
[62] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:40.001
[63] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:45.002

ok,到此为止就实现了动态修改定时任务的功能。

如何实现多线程并行任务

通过上面几个例子,可能细心的朋友已经发现了一个规律:上面所有的定时任务都是单线程的。

其实也正式如此,如官方文档中解释的:

By default, will be searching for an associated scheduler definition: either a unique TaskScheduler bean in the context, or a TaskScheduler bean named “taskScheduler” otherwise; the same lookup will also be performed for a ScheduledExecutorService bean. If neither of the two is resolvable, a local single-threaded default scheduler will be created and used within the registrar.

大概意思就是:默认情况下会检索是否指定了一个自定义的TaskScheduler,如果没有的情况下,会创建并使用一个本地单线程的任务调度器(线程池)。这一点,可以在ScheduledTaskRegistrar类(定时任务注册类)中加以佐证:

public void afterPropertiesSet() {
	this.scheduleTasks();
}

protected void scheduleTasks() {
	if (this.taskScheduler == null) {
		this.localExecutor = Executors.newSingleThreadScheduledExecutor();
		this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
	}
	// ...
}

那么我们如何将调度器改造成多线程形式的呢?继续向下看文档

When more control is desired, a @Configuration class may implement SchedulingConfigurer. This allows access to the underlying ScheduledTaskRegistrar instance.

意思很明了,我们可以通过实现SchedulingConfigurer接口,然后通过ScheduledTaskRegistrar类去注册自定义的线程池。在SchedulingConfigurer类中已经提供了一个setScheduler方法用来注册自定义的Scheduler Bean

public void setScheduler(Object scheduler) {
	Assert.notNull(scheduler, "Scheduler object must not be null");
	if (scheduler instanceof TaskScheduler) {
		this.taskScheduler = (TaskScheduler)scheduler;
	} else {
		if (!(scheduler instanceof ScheduledExecutorService)) {
			throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
		}

		this.taskScheduler = new ConcurrentTaskScheduler((ScheduledExecutorService)scheduler);
	}

}

接下来我们就按照这种方式扩展一个SchedulingConfigurer,代码如下:

@Component
public class MultiThreadSchedulingConfigurer implements SchedulingConfigurer {

    private AtomicInteger integer = new AtomicInteger(0);

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread() {
        System.out.println(String.format("[1] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread2() {
        System.out.println(String.format("[2] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Scheduled(cron = "0/1 * * * * ?")
    public void multiThread3() {
        System.out.println(String.format("[3] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(newExecutors());
    }

    @Bean(destroyMethod = "shutdown")
    private Executor newExecutors() {
        return Executors.newScheduledThreadPool(10, r -> new Thread(r, String.format("ZHYD-Task-%s", integer.incrementAndGet())));
    }
}

如代码所示,我们自定义一个容量为10得线程池,并且自定义一下线程池中线程得命名格式,同时为了方便查看,我们同时开启三个定时任务,在同样的时间同时执行,以此来观察定时器在并行执行时的具体线程分配情况

注意在上面的例子中自定义线程池时,使用了@Bean注解的(destroyMethod=“shutdown”)属性,这个属性可确保在Spring应用程序上下文本身关闭时正确关闭定时任务。

Note in the example above the use of @Bean(destroyMethod=“shutdown”). This ensures that the task executor is properly shut down when the Spring application context itself is closed.

[2] ZHYD-Task-2 exec @ 15:44:14.008
[1] ZHYD-Task-3 exec @ 15:44:14.008
[3] ZHYD-Task-1 exec @ 15:44:14.008
[2] ZHYD-Task-1 exec @ 15:44:15
[1] ZHYD-Task-3 exec @ 15:44:15
[3] ZHYD-Task-2 exec @ 15:44:15
[1] ZHYD-Task-4 exec @ 15:44:16.001
[3] ZHYD-Task-6 exec @ 15:44:16.001
[2] ZHYD-Task-5 exec @ 15:44:16.001
[1] ZHYD-Task-1 exec @ 15:44:17.002
[2] ZHYD-Task-7 exec @ 15:44:17.002
[3] ZHYD-Task-2 exec @ 15:44:17.002
[1] ZHYD-Task-1 exec @ 15:44:18.001

通过上面的结果可以看出,每个定时任务的执行线程都不在是固定的了。

总结

  1. springboot中通过@EnableScheduling注解开启定时任务,@Scheduled支持cronfixedDelayfixedRateinitialDelay四种定时任务的表达式
  2. springboot中通过实现SchedulingConfigurer接口并且重写configureTasks方法实现动态配置cron和多线程并行任务
10人推荐
随时随地看视频
慕课网APP

热门评论

赞!

查看全部评论