您的位置:首页 > 编程语言 > Java开发

【spring-Scheduled】浅谈spring scheduled定时任务

2019-12-24 16:08 363 查看

某厂面试归来,发现自己落伍了!>>>

#Round 1:使用Scheduled定义一个定时任务

定义一个java类,在需要定时执行的方法上加上@Scheduled注解

以下例子为使用cron表达式启动task1的方法,方法逻辑为每3s打印1句log,打印10句log以后退出。

public class Task1 {
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask1() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task1 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task1 is finished.");
}
}

配置并启动springboot可以看到以下log,可见task1被成功启动,但是有两个注意点:

  1. 启动该定时任务的线程名为pool-1-thread-1,可以理解为默认线程池pool的第一个线程
  2. 定时任务周期为5s,定时任务逻辑总时长为30s,但是第二轮定时任务该启动的时候并没有启动,而是等待直到第一轮的定时任务执行完成以后再开始第二轮的逻辑。
pool-1-thread-1 is running task1 @0times
pool-1-thread-1 is running task1 @1times
pool-1-thread-1 is running task1 @2times
pool-1-thread-1 is running task1 @3times
pool-1-thread-1 is running task1 @4times
pool-1-thread-1 is running task1 @5times
pool-1-thread-1 is running task1 @6times
pool-1-thread-1 is running task1 @7times
pool-1-thread-1 is running task1 @8times
pool-1-thread-1 is running task1 @9times
pool-1-thread-1 task1 is finished.
pool-1-thread-1 is running task1 @0times
pool-1-thread-1 is running task1 @1times
pool-1-thread-1 is running task1 @2times
······

#Round 2:使用Scheduled定义两个定时任务

我们将上面的类Task1稍微改一下新写一个Task2,新增一个相同的方法,并且在这个方法上加上相同的注解。

这样我们就有了两个定时任务,并且都是每5s启动一次,执行逻辑均为每3s打印1句log,打印10句以后退出。

public class Task2 {
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask2_1() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task2-1 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task2-1 is finished.");
}

@Scheduled(cron = "0/5 * * * * ?")
protected void doTask2_2() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task2-2 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task2-2 is finished.");
}
}

配置并启动springboot以后我们看到了以下输入内容:

但是这个输入结果好像和我们预想的不太一样,从输入的log中我们可以看出:

  1. 首先开启的是task2-1 (当然也有可能是task2-2,总之只会启动一个)
  2. 然后再task2-1执行结束以后task2-2才开始执行
  3. 并且两个定时任务是交替执行的
pool-1-thread-1 is running task2-1 @0times
pool-1-thread-1 is running task2-1 @1times
pool-1-thread-1 is running task2-1 @2times
pool-1-thread-1 is running task2-1 @3times
pool-1-thread-1 is running task2-1 @4times
pool-1-thread-1 is running task2-1 @5times
pool-1-thread-1 is running task2-1 @6times
pool-1-thread-1 is running task2-1 @7times
pool-1-thread-1 is running task2-1 @8times
pool-1-thread-1 is running task2-1 @9times
pool-1-thread-1 task2-1 is finished.
pool-1-thread-1 is running task2-2 @0times
pool-1-thread-1 is running task2-2 @1times
pool-1-thread-1 is running task2-2 @2times
pool-1-thread-1 is running task2-2 @3times
pool-1-thread-1 is running task2-2 @4times
pool-1-thread-1 is running task2-2 @5times
pool-1-thread-1 is running task2-2 @6times
pool-1-thread-1 is running task2-2 @7times
pool-1-thread-1 is running task2-2 @8times
pool-1-thread-1 is running task2-2 @9times
pool-1-thread-1 task2-2 is finished.
pool-1-thread-1 is running task2-1 @0times
pool-1-thread-1 is running task2-1 @1times
pool-1-thread-1 is running task2-1 @2times
······

为什么两个两个定时任务会同步执行呢?

仔细看一下log我们发现两个定时任务启动的线程都是pool-1-thread-1

疑问1:是不是因为默认的用于执行定时任务的线程池大小只有1呢?

疑问2:而在上一个例子中,只有一个定时任务,但是一次任务未结束第二次任务不开启,是不是也是因为线程池不够用呢?

#Round3:修改线程池大小使多个定时任务同时执行

org.springframework.scheduling包中我们发现了一个接口SchedulingConfigurer,字面意思计划任务配置,应该就是用来修改定时任务线程相关的内容。

接下来我们就用它来配置我们的springboot,实现这个类并实现configureTasks方法,通过setScheduler方法给scheduledTaskRegistrar对象设置一个线程池,线程数量设为5.

(注意此处因为是定时任务,所以必须使用Scheduled线程池,如果使用fixedcached线程池均会发生异常)

@Configuration
public class TaskConfigurator implements SchedulingConfigurer {
@Bean
public Task2 task2() {
return new Task2();
}

@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}

再次启动springboot我们看到了以下log输出,可见两个定时任务同时开启,都在执行10次以后结束,并且都在结束以后开启第二轮。

看来上面 疑问1 得到了解释,确实是因为线程池的数量限制了同时执行的任务。

然而在线程数量够用的情况下,为什么第二轮的逻辑没有启动呢?(疑问2 仍然没有得到合理的解释)

pool-1-thread-1 is running task2-2 @0times
pool-1-thread-2 is running task2-1 @0times
pool-1-thread-1 is running task2-2 @1times
pool-1-thread-2 is running task2-1 @1times
pool-1-thread-1 is running task2-2 @2times
pool-1-thread-2 is running task2-1 @2times
pool-1-thread-1 is running task2-2 @3times
pool-1-thread-2 is running task2-1 @3times
pool-1-thread-1 is running task2-2 @4times
pool-1-thread-2 is running task2-1 @4times
pool-1-thread-1 is running task2-2 @5times
pool-1-thread-2 is running task2-1 @5times
pool-1-thread-1 is running task2-2 @6times
pool-1-thread-2 is running task2-1 @6times
pool-1-thread-1 is running task2-2 @7times
pool-1-thread-2 is running task2-1 @7times
pool-1-thread-1 is running task2-2 @8times
pool-1-thread-2 is running task2-1 @8times
pool-1-thread-1 is running task2-2 @9times
pool-1-thread-2 is running task2-1 @9times
pool-1-thread-1 task2-2 is finished.
pool-1-thread-2 task2-1 is finished.
pool-1-thread-2 is running task2-1 @0times
pool-1-thread-3 is running task2-2 @0times
pool-1-thread-2 is running task2-1 @1times
pool-1-thread-3 is running task2-2 @1times
······

#Round4:异步任务,前一轮任务未结束不影响第二轮任务启动

我们继续在org.springframework.scheduling包里找找,发现有一个@Async@EnableAsync注解,看样子就是用来允许定时任务异步执行的。

我们修改一下Task2,得到Task3,其实只是在方法上加上了@Async注解:

public class Task3 {
@Async
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask3_1() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task3-1 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task3-1 is finished.");
}

@Async
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask3_2() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task3-2 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task3-2 is finished.");
}
}

同时修改一下springboot的配置文件,在类上加上@EnableAsync注解

@EnableAsync
@Configuration
public class TaskConfigurator implements SchedulingConfigurer {
@Bean
public Task3 task3() {
return new Task3();
}

@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}

配置好了springboot并启动后我们看到了以下log输出:

确实和我们预想的一样,加了@Async@EnableAsync注解以后定时任务异步执行了,前一轮未结束不影响新的一轮任务开启。到此上面的疑问2 也得到了解释,确实是因为任务的同步异步问题导致的。

task-1 is running task3-1 @0times
task-2 is running task3-2 @0times
task-1 is running task3-1 @1times
task-2 is running task3-2 @1times
task-3 is running task3-1 @0times
······
task-6 is running task3-2 @6times
task-4 is running task3-2 @8times
task-3 is running task3-1 @8times
task-8 is running task3-1 @5times
task-7 is running task3-2 @5times
task-2 task3-2 is finished.
task-2 is running task3-2 @0times
task-1 task3-1 is finished.
task-1 is running task3-1 @0times
task-5 is running task3-1 @7times
······

但是从log中发现,明明只配了5个线程,为什么却出现了task-8呢?而且和之前的pool-1-thread-x的命名方式也不一样了,应该不是同一个线程池。

疑问3:莫非同步线程池和异步线程池是两个相互独立的?

我们继续在org.springframework.scheduling包里找找,发现了接口AsyncConfigurer,根据刚刚的经验SchedulingConfigurer是用来配置同步任务线程池的,那么接口AsyncConfigurer是不是用来配置异步任务线程池的呢?

我们修改springboot配置文件,实现接口AsyncConfigurer,并实现其方法getAsyncExecutor

@Configuration
public class TaskConfigurator implements SchedulingConfigurer, AsyncConfigurer {
@Bean
public Task3 task3() {
return new Task3();
}

@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}

@Override
public Executor getAsyncExecutor() {
return Executors.newScheduledThreadPool(5);
}
}

配置完成启动springboot工程,我们看到以下log:

哈哈,执行的线程名又恢复成了pool-2-thread-x的格式,这里的pool-2应该是指第二个线程池吧。

确实和我们想的一样,这个接口确实是用来配置异步任务线程池的。

我们最初的目的虽然达到了,但是在实际的生产中会有一个问题发生:

  • 在一个项目中同步任务的数量是我们可以预见的,也就是说我们定时任务的数量设成和线程池大小一样是没有任何问题的,因为在一轮没有执行完成时不会开启下一轮任务。

  • 而异步任务由于两轮任务互不影响,而上一轮任务什么时候执行完成时无法预见的,所以执行异步任务的线程池需要比异步定时任务的数量多,不然就会占满线程导致新一轮或者共用异步线程池的定时任务无法开启。

疑问4:如何让多个异步定时任务运行在各自的线程池中呢?

pool-2-thread-1 is running task3-1 @0times
pool-2-thread-2 is running task3-2 @0times
pool-2-thread-1 is running task3-1 @1times
······
pool-2-thread-5 is running task3-1 @6times
pool-2-thread-3 is running task3-2 @8times
pool-2-thread-4 is running task3-1 @8times
pool-2-thread-2 task3-2 is finished.
pool-2-thread-2 is running task3-2 @0times
pool-2-thread-1 task3-1 is finished.
pool-2-thread-1 is running task3-1 @0times
pool-2-thread-5 is running task3-1 @7times
pool-2-thread-3 is running task3-2 @9times
pool-2-thread-4 is running task3-1 @9times
pool-2-thread-2 is running task3-2 @1times
pool-2-thread-1 is running task3-1 @1times
pool-2-thread-5 is running task3-1 @8times
pool-2-thread-3 task3-2 is finished.
pool-2-thread-3 is running task3-2 @0times
pool-2-thread-4 task3-1 is finished.
pool-2-thread-4 is running task3-1 @0times
pool-2-thread-1 is running task3-1 @2times

#Final Round:异步任务运行在各自的线程池中

回过头来看一下@Async注解,它允许有一个参数默认为空,从注释中可以发现可以通过这个参数为异步任务指定Executor或TaskExecutor

那我们修改Task3得到Task4,在两个方法的注解上加上线程池的spring IOC对象的name:

分别使用TaskExecutorPool1TaskExecutorPool2

public class Task4 {
@Async("TaskExecutorPool1")
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask4_1() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task4-1 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task4-1 is finished.");
}

@Async("TaskExecutorPool2")
@Scheduled(cron = "0/5 * * * * ?")
protected void doTask4_2() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running task4-2 @" + i + "times");
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName() + " task4-2 is finished.");
}
}

修改springboot配置文件,添加两个线程池并命名为TaskExecutorPool1TaskExecutorPool2

@EnableAsync
@Configuration
public class TaskConfigurator implements SchedulingConfigurer, AsyncConfigurer {
@Bean("TaskExecutorPool1")
public ThreadPoolTaskExecutor taskExecutor1() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setThreadNamePrefix("@@TaskExecutor1-");
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.initialize();
return taskExecutor;
}

@Bean("TaskExecutorPool2")
public ThreadPoolTaskExecutor taskExecutor2() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setThreadNamePrefix("@@TaskExecutor2-");
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.initialize();
return taskExecutor;
}

@Bean
public Task4 task4() {
return new Task4();
}
}

配置完成启动springboot,看到了我们想要的log输出:

  1. 两个异步定时任务在各自的线程池中运行互不影响
  2. 从线程名可以看出确实是我们指定的线程
  3. 由于线程池设置的核心线程数为5,运行时使用的线程数也为5

到此为止,我们最后一个疑问(疑问4)也得到了解释。

@@TaskExecutor1-1 is running task4-1 @0times
@@TaskExecutor2-1 is running task4-2 @0times
@@TaskExecutor1-1 is running task4-1 @1times
@@TaskExecutor2-1 is running task4-2 @1times
······
@@TaskExecutor2-5 is running task4-2 @3times
@@TaskExecutor1-5 is running task4-1 @3times
@@TaskExecutor2-2 is running task4-2 @8times
@@TaskExecutor1-2 is running task4-1 @8times
@@TaskExecutor1-4 is running task4-1 @5times
@@TaskExecutor2-4 is running task4-2 @5times
@@TaskExecutor1-1 task4-1 is finished.
@@TaskExecutor1-1 is running task4-1 @0times
@@TaskExecutor2-1 task4-2 is finished.
@@TaskExecutor2-1 is running task4-2 @0times
@@TaskExecutor2-3 is running task4-2 @7times
@@TaskExecutor1-3 is running task4-1 @7times
@@TaskExecutor1-5 is running task4-1 @4times
······
@@TaskExecutor1-5 is running task4-1 @5times
@@TaskExecutor2-5 is running task4-2 @5times
@@TaskExecutor2-2 task4-2 is finished.
@@TaskExecutor2-2 is running task4-2 @0times
@@TaskExecutor1-2 task4-1 is finished.
@@TaskExecutor1-2 is running task4-1 @0times
@@TaskExecutor1-4 is running task4-1 @7times
@@TaskExecutor2-4 is running task4-2 @7times
@@TaskExecutor1-1 is running task4-1 @2times

以上

本例中的所有代码已经上传至github

(window.slotbydup = window.slotbydup || []).push({ id: "u5894387", container: "_0hv0l6ey3zro", async: true });
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Spring Spring Boot