Spring中@Schedule和@Async注解实现细粒度定时任务

前言:多任务并发执行,同一任务的异步执行并且需要自定义线程池从而做到对线程得更加细粒度的控 制,请问你该如何实现?本文使用了@Schedule和@Async配合完成. 面向群体为初、中级Java开发工程师.阅读时间10min左右。

首先仅仅@Schedule发现用的是仅仅同一个线程会发生线程的阻塞.
@Component public class CurrThreadRun { @Scheduled(fixedRate = 3000) public void scheduledTask() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 end"); }@Scheduled(fixedRate = 3000) public void scheduledTask2() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 end"); } }

控制台的输出为以下内容,可以看到@shedule是个同步执行的任务
2022-03-06 16:26:39.744INFO 63252 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8091 (http) with context path '' 2022-03-06 16:26:39.749INFO 63252 --- [main] s.a.ScheduledAnnotationBeanPostProcessor : More than one TaskScheduler bean exists within the context, and none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' (possibly as an alias); or implement the SchedulingConfigurer interface and call ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: [scheduledTaskTwo, scheduledTaskOne] 2022-03-06 16:26:39 下午pool-1-thread-1===1 run 2022-03-06 16:26:39.753INFO 63252 --- [main] c.r.s.SpringbootDemoApplication: Started SpringbootDemoApplication in 1.198 seconds (JVM running for 1.407) 2022-03-06 16:26:45 下午pool-1-thread-1===1 end 2022-03-06 16:26:45 下午pool-1-thread-1===2 run 2022-03-06 16:26:51 下午pool-1-thread-1===2 end 2022-03-06 16:26:51 下午pool-1-thread-1===1 run

打开了ThreadPoolTaskScheduler源码发0现(Set the ScheduledExecutorService's pool size. Default is 1.
This setting can be modified at runtime, for example through JMX.)默认开启的线程数量为一。
public void setPoolSize(int poolSize) { Assert.isTrue(poolSize > 0, "'poolSize' must be 1 or higher"); if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) { ((ScheduledThreadPoolExecutor) this.scheduledExecutor).setCorePoolSize(poolSize); } this.poolSize = poolSize; }

【Spring中@Schedule和@Async注解实现细粒度定时任务】于是我们的代码又有了以下的改动。
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(2); //我这里设置的线程数是2,可以根据需求调整 return taskScheduler; }

做到这里我们实现了多任务并发执行,但是同一任务异步执行还没有实现.这个需求就更加简单了.
加上@Async之后,我们可以实现了同一任务的异步执行.
@Component public class CurrThreadRun { @Async @Scheduled(fixedRate = 3000) public void scheduledTask() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 end"); }@Async @Scheduled(fixedRate = 3000) public void scheduledTask2() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 end"); }@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(2); //我这里设置的线程数是2,可以根据需求调整 return taskScheduler; } }

输出为以下
2022-03-06 16:40:55.794INFO 63750 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8091 (http) with context path '' 2022-03-06 16:40:55.799INFO 63750 --- [taskScheduler-1] .s.a.AnnotationAsyncExecutionInterceptor : More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [taskScheduler, scheduledTaskTwo, scheduledTaskOne] 2022-03-06 16:40:55 下午SimpleAsyncTaskExecutor-2===2 run 2022-03-06 16:40:55 下午SimpleAsyncTaskExecutor-1===1 run 2022-03-06 16:40:55.801INFO 63750 --- [main] c.r.s.SpringbootDemoApplication: Started SpringbootDemoApplication in 1.261 seconds (JVM running for 1.581) 2022-03-06 16:40:58 下午SimpleAsyncTaskExecutor-3===1 run 2022-03-06 16:40:58 下午SimpleAsyncTaskExecutor-4===2 run 2022-03-06 16:41:01 下午SimpleAsyncTaskExecutor-6===2 run 2022-03-06 16:41:01 下午SimpleAsyncTaskExecutor-5===1 run 2022-03-06 16:41:01 下午SimpleAsyncTaskExecutor-2===2 end

但是这个输出的线程池名称让人难以理解,因为@Async是Async用的是默认的SimpleAsyncTaskExecutor作为线程池,所以 为了日志的可阅读性,
带着好奇心看了一下SimpleAsyncTaskExecutor这个线程池,文件开头就出现了一注释
TaskExecutor implementation that fires up a new Thread for each task, executing it asynchronously. Supports limiting concurrent threads through the "concurrencyLimit" bean property. By default, the number of concurrent threads is unlimited. NOTE: This implementation does not reuse threads! Consider a thread-pooling TaskExecutor implementation instead, in particular for executing a large number of short-lived tasks TaskExecutor 实现为每个任务启动一个新线程,异步执行它。 支持通过“concurrencyLimit”bean 属性限制并发线程。 默认情况下,并发线程数是无限的。 注意:此实现不重用线程! 考虑一个线程池 TaskExecutor 实现,特别是用于执行大量短期任务

所以该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。所以严格意义上说这个人并不是啥线程池我们要重新设置一下。
那么@Async是如何配置线程池的呢,这个我会另外再写一篇@Async配置线程池的文章进行描述.
为了满足前言的需求,我简单将@Async配置了ThreadPoolTaskScheduler,全部的代码如下图所示。
@Component public class CurrThreadRun {@Async("scheduledTaskOne") @Scheduled(fixedRate = 3000) public void scheduledTask() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===1 end"); }@Async("scheduledTaskTwo") @Scheduled(fixedRate = 3000) public void scheduledTask2() throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss a"); Date date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 run"); Thread.sleep(6 * 1000); date = new Date(); System.out.println(sdf.format(date) + Thread.currentThread().getName() + "===2 end"); }@Bean ThreadPoolTaskScheduler scheduledTaskTwo() { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(2); threadPoolTaskScheduler.setAwaitTerminationSeconds(60); threadPoolTaskScheduler.setThreadNamePrefix("TASK_SCHEDULER_SECOND-AAA-"); return threadPoolTaskScheduler; }@Bean ThreadPoolTaskScheduler scheduledTaskOne() { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(2); threadPoolTaskScheduler.setThreadNamePrefix("TASK_SCHEDULER_SECOND-BBB-"); return threadPoolTaskScheduler; } }

得到了输出的控制台如下所示:
2022-03-06 17:10:18.509INFO 64755 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8091 (http) with context path '' 2022-03-06 17:10:18.513INFO 64755 --- [main] s.a.ScheduledAnnotationBeanPostProcessor : More than one TaskScheduler bean exists within the context, and none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' (possibly as an alias); or implement the SchedulingConfigurer interface and call ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: [scheduledTaskTwo, scheduledTaskOne] 2022-03-06 17:10:18.516INFO 64755 --- [main] c.r.s.SpringbootDemoApplication: Started SpringbootDemoApplication in 1.391 seconds (JVM running for 1.613) 2022-03-06 17:10:18 下午TASK_SCHEDULER_SECOND-BBB-1===1 run 2022-03-06 17:10:18 下午TASK_SCHEDULER_SECOND-AAA-1===2 run 2022-03-06 17:10:21 下午TASK_SCHEDULER_SECOND-BBB-2===1 run 2022-03-06 17:10:21 下午TASK_SCHEDULER_SECOND-AAA-2===2 run 2022-03-06 17:10:24 下午TASK_SCHEDULER_SECOND-BBB-1===1 end 2022-03-06 17:10:24 下午TASK_SCHEDULER_SECOND-AAA-1===2 end 2022-03-06 17:10:24 下午TASK_SCHEDULER_SECOND-BBB-1===1 run 2022-03-06 17:10:24 下午TASK_SCHEDULER_SECOND-AAA-1===2 run

参考资料
  1. SpringBoot @Scheduled注解使用: 同步/异步同一任务及多任务并发执行
  2. Spring使用@Async注解

    推荐阅读