title: springboot定時(shí)任務(wù)
copyright: true
categories: springmvc和springboot
tags: 配置文件
password:
- 一喜最、白拿拿項(xiàng)目中需要每天凌晨統(tǒng)計(jì)一次昨天一天的邀請(qǐng)排行榜,與定時(shí)任務(wù)有關(guān)代碼示例如下:
@Component
public class ScheduleHandler {
private final IChannelSourceConfigService channelSourceConfigService;
public ScheduleHandler(IChannelSourceConfigService channelSourceConfigService) {
this.channelSourceConfigService = channelSourceConfigService;
}
@Scheduled(cron = "0 7 0 * * ?")
public void setShowkerCountCache() {
channelSourceConfigService.refreshAll();
}
}
- 二虏等、當(dāng)有多個(gè)定時(shí)器的時(shí)候 需要異步使用 增加定時(shí)器線程池配置
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod = "shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
}
-
三、詳細(xì)內(nèi)容
Spring 定時(shí)任務(wù)實(shí)例
Spring 中使用定時(shí)任務(wù)很簡(jiǎn)單鼎俘,只需要@EnableScheudling 注解啟用即可,并不要求是一個(gè) Spring Mvc 的項(xiàng)目。
對(duì)于一個(gè) Spring Boot 項(xiàng)目,使用定時(shí)任務(wù)的簡(jiǎn)單方式如下:
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 是必須的垄琐。默認(rèn)時(shí)定時(shí)任務(wù)的線程是由 Executors.defaultThreadFactory() 產(chǎn)生的,線程名稱是 "pool-NUMBER-thread-...", 關(guān)鍵是線程的 daemon 屬性為 false, 阻止了主線程的退出经柴,使得任務(wù)能一遍遍執(zhí)行狸窘。
SchedulRunner.java
@Component
public class ScheduleRunner {
@Scheduled(fixedDelay = 5000)
public void job1() {
System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now());
}
}
順帶提一下注解@Scheduled的各個(gè)屬性
- cron: 以 UN*X 的 cron 的方式定義 job, 如 "0 * * * * NON-FRI"
- fixedRate: 每次任務(wù)啟動(dòng)時(shí)的間隔時(shí)間,fixedRateString口锭,意義是一樣,只是可以通過(guò)外部來(lái)定義介杆,如 fixedRateString = "${job1.fixed.rate}"
- fixedDelay: 上次任務(wù)結(jié)束后間隔多少時(shí)間再啟動(dòng)下一次任務(wù)鹃操,這樣避免前一個(gè)任務(wù)尚未結(jié)束又啟動(dòng)下一個(gè)任務(wù),fixedDelayString 類似 fixedRateString
- intialDelay: 程序啟動(dòng)后至任務(wù)首次執(zhí)行時(shí)的間隔時(shí)間春哨,針對(duì) fixedRate(fixedRateString), fixedDelay(fixedDelayString)
- zone: 給 cron 表達(dá)式用的時(shí)區(qū)
- 注意, 以上的時(shí)間都是毫秒
?啟動(dòng)這個(gè) Spring Boot 項(xiàng)目荆隘,可以看到 job1 每隔五分鐘執(zhí)行一次,并且全部由一個(gè)線程來(lái)執(zhí)行
?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
?居然總是同一個(gè)線程
?如果我們把上面的 fixedDelay 改成 fixedRate, 并且用 Thread.sleep(20000) 來(lái)模擬單次任務(wù)耗時(shí) 20 秒赴背,試圖讓上次任務(wù)還在進(jìn)行當(dāng)中執(zhí)行下一次任務(wù)
@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) {
}
}
}
?執(zhí)行后椰拒,發(fā)現(xiàn)事與愿為
?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
??并非每五秒啟動(dòng)下一個(gè)任務(wù),而是每隔20 秒凰荚,原來(lái)是只有一個(gè)線程來(lái)執(zhí)行所有任務(wù)燃观,后面的任務(wù)必須等前一個(gè)任務(wù)釋放出了線程才能得到執(zhí)行。我們可以理解為 Spring 在任務(wù)調(diào)度時(shí)便瑟,fixedRate, fixedDelay 或 cron 只是決定提交任務(wù)到線程池的時(shí)刻缆毁,至于真正執(zhí)行任務(wù)的時(shí)間就看有沒(méi)有空閑的線程,因此最終決定于線程池的配置到涂。
?同樣脊框,如果我們?cè)赟cheduleRunner 中聲明兩個(gè)任務(wù)(后續(xù)的執(zhí)行輸出結(jié)果都以這兩個(gè)任務(wù)為例)
@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());
}
}
執(zhí)行的效果是下面那樣的
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
也是因?yàn)槭冀K只有一個(gè)線程的緣故颁督,任務(wù)調(diào)度無(wú)法按照預(yù)定的要求,job1 和 job2 不能同時(shí)進(jìn)行浇雹,更別說(shuō) job1 或是 job2 的前后兩次任務(wù)同時(shí)進(jìn)行沉御。job2 每次要等待 job1 執(zhí)行完釋放出線程來(lái)執(zhí)行,所以不管 fixedDelay 或 fixedRate 配置多小的時(shí)間間隔昭灵,中間都至少要等 20 秒吠裆。
既然我們知曉了是單一線程的原因,那么再追根究底看看虎锚,以及解決辦法是什么硫痰?
如何創(chuàng)建任務(wù)線程的?
查看源代碼是最有效的窜护,采用順藤摸瓜的辦法效斑,從 @EnableScheduling 起,在 EnableScheduling 中找到 @see ScheduledAnnotationBeanPostProcessor, 來(lái)到ScheduledAnnotationBeanPostProcessor.setScheduler(Object scheduler)方法的 JavaDoc
說(shuō)的是定時(shí)任務(wù)需要一個(gè)線程池(TaskScheduler 或 ScheduledExecutorService) 來(lái)執(zhí)行柱徙,Spring 會(huì)通過(guò)以下順序去獲得 TaskScheduler 或是 ScheduledExecutorService 包裝為 TaskScheduler 實(shí)例
1缓屠、類型為 TaskScheduler 的唯一 Bean
2、如果第 1 步未找到护侮,或找到多個(gè)就嘗試查找名稱為 "taskScheduler", 類型為 TaskScheduler 的 Bean
3敌完、查找類型為 ScheduledExecutorService 的 Bean, 并包裝為 TaskScheduler 實(shí)例
4、如果第 3 步未到羊初,或找到多個(gè)就嘗試查找 名稱為"taskScheduler", 類型為 ScheduledExecutorService 的 Bean, 并包裝為 TaskScheduler 實(shí)例
也就是可以定一唯的類型為 TaskScheduler 或 ScheduledExecutorService 的 Bean, 或者是名稱為 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 實(shí)例滨溉。
查找 TaskScheduler 的方法是ScheduledAnnotationBeanPostProcessor.finishRegistration(), 點(diǎn)接該鏈接查看源代碼。
找到了 TaskScheduler 或 ScheduledExecutorService 后設(shè)置 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());
}
}
對(duì) ScheduledExecutorService 的包裝是通過(guò) ConsurrentTaskScheduler 類晦攒。
而在 ScheduledTaskRegistrar 中注冊(cè)任務(wù)是由 scheduleTasks() 實(shí)現(xiàn)的,
protected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
......
}
這才看到為什么默認(rèn)情況下 Spring 用單線程來(lái)執(zhí)行所有的任務(wù), 因?yàn)?Spring 未定義 TaskScheduler 和 ScheduledExecutorService 這兩個(gè)實(shí)例得哆。此名脯颜,上面的
Executors.newSingleThreadScheduledExecutor()
最終會(huì)調(diào)用 Executors.defaultThreadFactory() 來(lái)創(chuàng)建 daemon 為 false 的線程。
- 四贩据、提供自定義的任務(wù)線程池
一般來(lái)說(shuō)栋操,只用一個(gè)線程來(lái)執(zhí)行所有的任務(wù)是滿足不了我們的需求的,除非項(xiàng)目中只有一個(gè)任務(wù)時(shí)的以下兩種情況
? 用 fixedDelay 來(lái)配置的
? fixedRate 或 cron, 并且在時(shí)間間隔內(nèi)每次任務(wù)必須能執(zhí)行完成
知道了來(lái)龍去脈饱亮,就可以參考上面 1, 2, 3, 4 的順序來(lái)定義一個(gè)自己的 TaskScheduler 來(lái) ScheduledExecutorService 實(shí)例
? 類型為 TaskScheduler 或 ScheduledExecutorService 的實(shí)例
? 名稱為 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 實(shí)例
TaskScheduler 接口有三個(gè)實(shí)現(xiàn)矾芙,分別是 ThreadPoolTaskScheduler,
ConcurrentTaskScheduler, 和 DefaultMangedTaskScheduler(繼承自 ConsurrentTaskScheduler)
ScheduledExecutorService 接口有兩個(gè)實(shí)現(xiàn)類,分別是 ScheduledThreadPoolExecutor
DelegatedScheduledExecutorService
下面是幾個(gè)例子近上,可在前面的 Application 類中配置一個(gè) @Bean, 代碼如下
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
return taskScheduler;
}
再次運(yùn)行
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-3,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-4,5,main], job2@23:21:34.327
現(xiàn)在分別由不同的的線程來(lái)執(zhí)行各自的任務(wù)蠕啄,互不干涉,每次任務(wù)由誰(shuí)來(lái)執(zhí)行只取決于池中的空閑線程。現(xiàn)在終于是 job1 每 25(20+5) 秒歼跟, job2 每 5 秒執(zhí)行一次和媳。應(yīng)用中應(yīng)根據(jù)任務(wù)間隔與每個(gè)任務(wù)執(zhí)行時(shí)長(zhǎng)來(lái)配置線程池的大小。此時(shí)線程池的名稱是 TaskScheduler Bean 的名稱哈街,所以我們想改變線程池名稱的話可以命一個(gè)新的 Bean 名稱留瞳,改方法名或是指定 @Bean 的 name 屬性,如
@Bean(name = "TaskPool")
public TaskScheduler taskScheduler() {
.....
}
那么執(zhí)行后打印的線程名稱是
Thread[TaskPool-2,5,main], job1@23:26:09.330
Thread[TaskPool-1,5,main], job2@23:26:09.330
線程 daemon 應(yīng)該是 false, 除非主線程自己不退
注意骚秦,如果是自己定義的線程池不能把線程的 daemon 設(shè)置為 true, 否則主線程很快退出進(jìn)而整個(gè)進(jìn)程結(jié)束她倘,那就不是定時(shí)任務(wù)了。例如我們聲明如下的 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 導(dǎo)致主線程很快退出作箍,從而進(jìn)程退出
return thread;
}));
return taskScheduler;
}
執(zhí)行程序后的效果可能是這樣的
這還比較幸運(yùn)硬梁,任務(wù)被執(zhí)行了一次镰烧,進(jìn)程退出了钱雷,也有可能一次任務(wù)都無(wú)法執(zhí)行埂息,如果是 fixedDelay 稍長(zhǎng)的任務(wù)更是不可能得到一次執(zhí)行的機(jī)會(huì)進(jìn)程就退出了载萌。如果你的主線程自己控制了永不退出也是可行的。
這種情況下普筹,我們一般是不會(huì)這么干 -- 把線程的 daemon 設(shè)置為 true爆哑,這也就是為什么 ConcurrentTaskScheduler 接收的是一個(gè) ScheduledExecutorService 參數(shù)什猖。
名稱 "taskScheduler" 或類型 "ScheduledExecutorService" 來(lái)查找相應(yīng)的 Bean, 如果都沒(méi)有找到牧愁,就會(huì)使用默認(rèn)的單線程的 scheduler 來(lái) 執(zhí)行任務(wù)素邪,這就是我們之前看到的效果。
@Scheduled 與 @Async
還是有必要提到一種情況猪半,@Scheduled 和 @Async 是可以共存的兔朦。可以試著這么做
? 給 Application 類加上 @EnableAsync
? 給 ScheduleRunner 的 job1() 和 job2() 方法加上注解 @Async
執(zhí)行后
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 并不使用線程池來(lái)執(zhí)行任務(wù)磨确,而是每次創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)沽甥,由于 job1() 和 job2() 兩方法是異步的,所以 fixedDelay 的效果與 fixedRate 是一樣的俐填,因?yàn)榉椒ㄒ徽{(diào)用即認(rèn)為是結(jié)束安接,馬上就安排下一次執(zhí)行的時(shí)間翔忽。如果想用 fixedDelay 讓前后兩次任務(wù)是有關(guān)聯(lián)的英融,方法不能為 @Async.
給自己備注一下:
用 @Scheduled 標(biāo)注的方法最后是包裝到ScheduledMethodRunnable 中被執(zhí)行的,它是一個(gè) Runnable 接口的實(shí)現(xiàn)
Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);