内容简介:Spring 定时任务(Schedule) 和线程
Spring 定时任务实例
Spring 中使用定时任务很简单,只需要 @EnableScheudling 注解启用即可,并不要求是一个 Spring Mvc 的项目。对于一个 Spring Boot 项目,使用定时任务的简单方式如下:
pom.xml 中
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
Application.java
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableScheduling 是必须的。 默认时定时任务的线程是由 Executors.defaultThreadFactory() 产生的,线程名称是 "pool-NUMBER-thread-...", 关键是线程的 daemon 属性为 false, 阻止了主线程的退出,使得任务能一遍遍执行 。
SchedulRunner.java
@Component public class ScheduleRunner { @Scheduled(fixedDelay = 5000) public void job1() { System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); } }
顺带提一下注解 @Scheduled 的各个属性
- cron: 以 UN*X 的 cron 的方式定义 job, 如 "0 * * * * NON-FRI"
- fixedRate: 每次任务启动时的间隔时间,fixedRateString,意义是一样,只是可以通过外部来定义,如 fixedRateString = "${job1.fixed.rate}"
- fixedDelay: 上次任务结束后间隔多少时间再启动下一次任务,这样避免前一个任务尚未结束又启动下一个任务,fixedDelayString 类似 fixedRateString
- intialDelay: 程序启动后至任务首次执行时的间隔时间,针对 fixedRate(fixedRateString), fixedDelay(fixedDelayString)
- zone: 给 cron 表达式用的时区
注意, 以上的时间都是毫秒
启动这个 Spring Boot 项目,可以看到 job1 每隔五分钟执行一次,并且全部由一个线程来执行
Thread[pool-1-thread-1,5,main], job1@21:57:46.822
Thread[pool-1-thread-1,5,main], job1@21:57:51.831
Thread[pool-1-thread-1,5,main], job1@21:57:56.836
Thread[pool-1-thread-1,5,main], job1@21:58:01.841
居然总是同一个线程
如果我们把上面的 fixedDelay 改成 fixedRate, 并且用 Thread.sleep(20000) 来模拟单次任务耗时 20 秒,试图让上次任务还在进行当中执行下一次任务
@Component public class ScheduleRunner { @Scheduled(fixedRate = 5000) public void job1() { System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); try { Thread.sleep(20000); } catch (InterruptedException e) { } } }
执行后,发现事与愿为
Thread[pool-1-thread-1,5,main], job1@21:58:57.564
Thread[pool-1-thread-1,5,main], job1@21:59:17.572
Thread[pool-1-thread-1,5,main], job1@21:59:37.575
Thread[pool-1-thread-1,5,main], job1@21:59:57.580
并非每五秒启动下一个任务,而是每隔 20 秒,原来是只有一个线程来执行所有任务,后面的任务必须等前一个任务释放出了线程才能得到执行。
同样,如果我们在 ScheduleRunner 中声明两个任务( 后续的执行输出结果都以这两个任务为例 )
@Component public class ScheduleRunner { @Scheduled(fixedDelay = 5000) public void job1() { System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); try { Thread.sleep(20000); } catch (InterruptedException e) { } } @Scheduled(fixedDelay = 5000) public void job2() { System.out.println(Thread.currentThread() + ", job2@" + LocalTime.now()); } }
执行的效果是下面那样的
Thread[pool-1-thread-1,5,main], job2@22:05:12.236
Thread[pool-1-thread-1,5,main], job1@22:05:12.241
Thread[pool-1-thread-1,5,main], job2@22:05:32.244
Thread[pool-1-thread-1,5,main], job1@22:05:37.246
Thread[pool-1-thread-1,5,main], job2@22:05:57.250
Thread[pool-1-thread-1,5,main], job1@22:06:02.253
也是因为始终只有一个线程的缘故,任务调度无法按照预定的要求,job1 和 job2 不能同时进行,更别说 job1 或是 job2 的前后两次任务同时进行。job2 每次要等待 job1 执行完释放出线程来执行,所以不管 fixedDelay 或 fixedRate 配置多小的时间间隔,中间都至少要等 20 秒。
既然我们知晓了是单一线程的原因,那么再追根究底看看,以及解决办法是什么?
如何创建任务线程的?
查看源代码是最有效的,采用顺藤摸瓜的办法,从 @EnableScheduling 起,在 EnableScheduling 中找到 @see ScheduledAnnotationBeanPostProcessor, 来到 ScheduledAnnotationBeanPostProcessor.setScheduler(Object scheduler) 方法的 JavaDoc
说的是定时任务需要一个线程池(TaskScheduler 或 ScheduledExecutorService) 来执行,Spring 会通过以下顺序去获得 TaskScheduler 或是 ScheduledExecutorService 包装为 TaskScheduler 实例
- 类型为 TaskScheduler 的唯一 Bean
- 如果第 1 步未找到,或找到多个就尝试查找名称为 "taskScheduler", 类型为 TaskScheduler 的 Bean
- 查找类型为 ScheduledExecutorService 的 Bean, 并包装为 TaskScheduler 实例
- 如果第 3 步未到,或找到多个就尝试查找 名称为"taskScheduler", 类型为 ScheduledExecutorService 的 Bean, 并包装为 TaskScheduler 实例
也就是可以定一唯的类型为 TaskScheduler 或 ScheduledExecutorService 的 Bean, 或者是名称为 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 实例。
查找 TaskScheduler 的方法是 ScheduledAnnotationBeanPostProcessor.finishRegistration() , 点接该链接查看源代码。
找到了 TaskScheduler 或 ScheduledExecutorService 后设置 Scheduler 的代码如下,在 ScheduledTaskRegistrar 类中
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) { this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler)); } else { throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass()); } }
对 ScheduledExecutorService 的包装是通过 ConsurrentTaskScheduler 类。
而在 ScheduledTaskRegistrar 中注册任务是由 scheduleTasks() 实现的,
protected void scheduleTasks() { if (this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); } ...... }
这才看到为什么默认情况下 Spring 用单线程来执行所有的任务, 因为 Spring 未定义 TaskScheduler 和 ScheduledExecutorService 这两个实例。此名,上面的
Executors.newSingleThreadScheduledExecutor()
最终会调用 Executors.defaultThreadFactory() 来创建 daemon 为 false 的线程。
提供自定义的任务线程池
一般来说,只用一个线程来执行所有的任务是满足不了我们的需求的,除非项目中只有一个任务时的以下两种情况
- 用 fixedDelay 来配置的
- fixedRate 或 cron, 并且在时间间隔内每次任务必须能执行完成
知道了来龙去脉,就可以参考上面 1, 2, 3, 4 的顺序来定义一个自己的 TaskScheduler 来 ScheduledExecutorService 实例
- 类型为 TaskScheduler 或 ScheduledExecutorService 的实例
- 名称为 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 实例
TaskScheduler 接口有三个实现,分别是 ThreadPoolTaskScheduler, ConcurrentTaskScheduler, 和 DefaultMangedTaskScheduler(继承自 ConsurrentTaskScheduler)
ScheduledExecutorService 接口有两个实现类,分别是 ScheduledThreadPoolExecutor 和 DelegatedScheduledExecutorService
下面是几个例子,可在前面的 Application 类中配置一个 @Bean, 代码如下
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(2); return taskScheduler; }
再次运行
Thread[taskScheduler-1,5,main], job2@23:21:09.307
Thread[taskScheduler-2,5,main], job1@23:21:09.307
Thread[taskScheduler-1,5,main], job2@23:21:14.315
Thread[taskScheduler-1,5,main], job2@23:21:19.318
Thread[taskScheduler-1,5,main], job2@23:21:24.322
Thread[taskScheduler-1,5,main], job2@23:21:29.326
Thread[taskScheduler-2,5,main], job1@23:21:34.320
Thread[taskScheduler-1,5,main], job2@23:21:34.327
现在分别由两个线程来执行各自的任务,互不干涉,job1 每 25(20+5) 秒, job2 每 5 秒执行一次。此时线程池的名称是 TaskScheduler Bean 的名称,所以我们想改变线程池名称的话可以命一个新的 Bean 名称,改方法名或是指定 @Bean 的 name 属性,如
@Bean(name = "TaskPool") public TaskScheduler taskScheduler() { ..... }
那么执行后打印的线程名称是
Thread[TaskPool-2,5,main], job1@23:26:09.330
Thread[TaskPool-1,5,main], job2@23:26:09.330
线程 daemon 应该是 false, 除非主线程自己不退
注意,如果是自己定义的线程池不能把线程的 daemon 设置为 true, 否则主线程很快退出进而整个进程结束,那就不是定时任务了。例如我们声明如下的 taskScheduler
@Bean public TaskScheduler taskScheduler() { AtomicInteger number = new AtomicInteger(1); ConcurrentTaskScheduler taskScheduler = new ConcurrentTaskScheduler( Executors.newScheduledThreadPool(3, r -> { Thread thread = new Thread(r); thread.setName("TaskPool-thread-" + number.getAndIncrement()); thread.setDaemon(true); //daemon 为 true 导致主线程很快退出,从而进程退出 return thread; })); return taskScheduler; }
执行程序后的效果可能是这样的
这还比较幸运,任务被执行了一次,进程退出了,也有可能一次任务都无法执行,如果是 fixedDelay 稍长的任务更是不可能得到一次执行的机会进程就退出了。如果你的主线程自己控制了永不退出也是可行的。
这种情况下,我们一般是不会这么干 -- 把线程的 daemon 设置为 true,这也就是为什么 ConcurrentTaskScheduler 接收的是一个 ScheduledExecutorService 参数。
名称 "taskScheduler" 或类型 "ScheduledExecutorService" 来查找相应的 Bean, 如果都没有找到,就会使用默认的单线程的 scheduler 来 执行任务,这就是我们之前看到的效果。
@Scheduled 与 @Async
还是有必要提到一种情况,@Scheduled 和 @Async 是可以共存的。可以试着这么做
- 给 Application 类加上 @EnableAsync
- 给 ScheduleRunner 的 job1() 和 job2() 方法加上注解 @Async
执行后
Thread[SimpleAsyncTaskExecutor-1,5,main], job1@00:13:36.763
Thread[SimpleAsyncTaskExecutor-2,5,main], job2@00:13:36.763
Thread[SimpleAsyncTaskExecutor-3,5,main], job1@00:13:41.738
Thread[SimpleAsyncTaskExecutor-4,5,main], job2@00:13:41.738
Thread[SimpleAsyncTaskExecutor-5,5,main], job1@00:13:46.742
Thread[SimpleAsyncTaskExecutor-6,5,main], job2@00:13:46.742
SimpleAsyncTaskExecutor 并不使用线程池来执行任务,而是每次创建新的线程来执行任务,由于 job1() 和 job2() 两方法是异步的,所以 fixedDelay 的效果与 fixedRate 是一样的,因为方法一调用即认为是结束,马上就安排下一次执行的时间。如果想用 fixedDelay 让前后两次任务是有关联的,方法不能为 @Async.
给自己备注一下:
用 @Scheduled 标注的方法最后是包装到 ScheduledMethodRunnable 中被执行的,它是一个 Runnable 接口的实现
Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 并发编程那些事儿(一):任务与线程
- 风铃虫 2.1.0 发布,支持多任务多线程
- 任务与队列 iOS之多线程GCD(一)
- Java实现终止线程池中正在运行的定时任务
- CentOS下使用supervisor配置Laravel多线程任务队列
- JAVA线程池原理源码解析—为什么启动一个线程池,提交一个任务后,Main方法不会退出?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Haskell趣学指南
[斯洛文尼亚] Miran Lipovaca / 李亚舟、宋方睿 / 人民邮电出版社 / 2014-1
《haskell趣学指南》是一本讲解haskell这门函数式编程语言的入门指南,语言通俗易懂,插图生动幽默,示例短小清晰,结构安排合理。书中从haskell的基础知识讲起,涵盖了所有的基本概念和语法,内容涉及基本语法、递归、类型和类型类、函子、applicative 函子、monad、zipper及所有haskell重要特性和强大功能。 《haskell趣学指南》适合对函数式编程及haske......一起来看看 《Haskell趣学指南》 这本书的介绍吧!