Spring之兩種任務(wù)調(diào)度Scheduled和Async詳解
1、Spring調(diào)度的兩種方式
Spring提供了兩種后臺(tái)任務(wù)的方法,分別是:
- 調(diào)度任務(wù),@Schedule
- 異步任務(wù),@Async
當(dāng)然,使用這兩個(gè)是有條件的,需要在spring應(yīng)用的上下文中聲明
<task:annotation-driven/>當(dāng)然,如果我們是基于java配置的,需要在配置哪里加多EnableScheduling和@EnableAsync 就像下面這樣
@EnableScheduling
@EnableAsync
public class WebAppConfig {
....
}
除此之外,還是有第三方庫(kù)可以調(diào)用的,例如Quartz.
2、@Schedule
先看下@Schedule怎么調(diào)用再說
public final static long ONE_DAY = 24 * 60 * 60 * 1000;
public final static long ONE_HOUR = 60 * 60 * 1000;
@Scheduled(fixedRate = ONE_DAY)
public void scheduledTask() {
System.out.println(" 我是一個(gè)每隔一天就會(huì)執(zhí)行一次的調(diào)度任務(wù)");
}
@Scheduled(fixedDelay = ONE_HOURS)
public void scheduleTask2() {
System.out.println(" 我是一個(gè)執(zhí)行完后,隔一小時(shí)就會(huì)執(zhí)行的任務(wù)");
}
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should execute periodically
}
@Scheduled(cron = "0 0/1 * * * ? ")
public void ScheduledTask3() {
System.out.println(" 我是一個(gè)每隔一分鐘就就會(huì)執(zhí)行的任務(wù)");
}
需要注意的
- 關(guān)于最后一個(gè),在指定時(shí)間執(zhí)行的任務(wù),里面使用的是Cron表達(dá)式,同時(shí)我們看到了兩個(gè)不一樣的面孔fixedDelay& fixedRate,前者fixedDelay表示在指定間隔運(yùn)行程序,例如這個(gè)程序在今晚九點(diǎn)運(yùn)行程序,跑完這個(gè)方法后的一個(gè)小時(shí),就會(huì)再執(zhí)行一次,而后者fixedDelay者是指,這個(gè)函數(shù)每隔一段時(shí)間就會(huì)被調(diào)用(我們這里設(shè)置的是一天),不管再次調(diào)度的時(shí)候,這個(gè)方法是在運(yùn)行還是結(jié)束了。而前者就要求是函數(shù)運(yùn)行結(jié)束后開始計(jì)時(shí)的,這就是兩者區(qū)別。
- 這個(gè)還有一個(gè)initialDelay的參數(shù),是第一次調(diào)用前需要等待的時(shí)間,這里表示被調(diào)用后的,推遲一秒再執(zhí)行,這適合一些特殊的情況。
- 我們?cè)趕erviceImpl類寫這些調(diào)度任務(wù)時(shí)候,也需要在這些我們定義的serviceInterface的借口中寫多這個(gè)接口,要不然會(huì)爆 but not found in any interface(s) for bean JDK proxy.Either pull the method up to an interface or
3、@Async
有時(shí)候我們會(huì)調(diào)用一些特殊的任務(wù),任務(wù)會(huì)比較耗時(shí),重要的是,我們不管他返回的后果。這時(shí)候我們就需要用這類的異步任務(wù)啦,調(diào)用后就讓他去跑,不堵塞主線程,我們繼續(xù)干別的。代碼像下面這樣:
public void AsyncTask(){
@Async
public void doSomeHeavyBackgroundTask(int sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async
public Future<String> doSomeHeavyBackgroundTask() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public void printLog() {
System.out.println(" i print a log ,time=" + System.currentTimeMillis());
}
}
我們寫個(gè)簡(jiǎn)單的測(cè)試類來測(cè)試下
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = AsycnTaskConfig.class) //要聲明@EnableASync
public class AsyncTaskTest {
@Autowired
AsyncTask asyncTask;
@Test
public void AsyncTaskTest() throws InterruptedException {
if (asyncTask != null) {
asyncTask.doSomeHeavyBackgroundTask(4000);
asyncTask.printLog();
Thread.sleep(5000);
}
}
}
這感覺比我們手動(dòng)開多一個(gè)線程方便多了,不想異步的話直接把@Async去掉就可以了,另外如果你想要返回個(gè)結(jié)果的,這需要加多個(gè)Future<>,關(guān)于這個(gè)Future,完全可以寫多幾篇文章介紹,順便把FutureTask介紹了。如果想修改Spring boot的默認(rèn)線程池配置,可以實(shí)現(xiàn)AsyncConfigurer.
需要注意的:
相對(duì)于@scheduled,這個(gè)可以有參數(shù)和返回個(gè)結(jié)果,因?yàn)檫@個(gè)是我們調(diào)用的,而調(diào)度的任務(wù)是spring調(diào)用的。
異步方法不能內(nèi)部調(diào)用,只能像上面那樣,外部調(diào)用,否則就會(huì)變成阻塞主線程的同步任務(wù)啦!這個(gè)坑我居然跳下去了!例如下面這樣的。
public void AsyncTask(){
public void fakeAsyncTaskTest(){
doSomeHeavyBackgroundTask(4000);
printLog();
//你會(huì)發(fā)現(xiàn),當(dāng)你像這樣內(nèi)部調(diào)用的時(shí)候,居然是同步執(zhí)行的,不是異步的??!
}
@Async
public void doSomeHeavyBackgroundTask(int sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void printLog() {
System.out.println(" i print a log ");
}
}
- 另外一點(diǎn)就是不要重復(fù)的掃描,這也會(huì)導(dǎo)致異步無效,具體的可以看這個(gè)stackoveflow的spring-async-not-working Issue。
- 關(guān)于異常處理,難免在這個(gè)異步執(zhí)行過程中有異常發(fā)生,對(duì)于這個(gè)問題,spring提供的解決方案如下,實(shí)現(xiàn)
AsyncUncaughtExceptionHandler接口。
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
寫好我們的異常處理后,我們需要配置一下,告訴spring,這個(gè)異常處理就是我們?cè)谶\(yùn)行異步任務(wù)時(shí)候,拋出錯(cuò)誤時(shí)的異常終結(jié)者
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean
public AsyncTask asyncBean() {
return new AsyncTask();
}
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncUncaughtExceptionHandler();
}
}
4、Quartz登場(chǎng)
處理這兩個(gè)外,還有一個(gè)和spring整合的第三方庫(kù)叫Quartz
看了下官網(wǎng)的使用簡(jiǎn)介,也是挺逗的,現(xiàn)在都習(xí)慣用maven,gradle之類來關(guān)系這些依賴了,他還叫人下載,也是不知為何,詳情點(diǎn)擊->http://quartz-scheduler.org/documentation/quartz-2.2.x/quick-start
估計(jì)有可能是因?yàn)闆]再維護(hù)了的原因吧,看了下,最新版2.2居然是Sep, 2013更新的…
居然是停更的,不過Quartz作為一個(gè)企業(yè)級(jí)應(yīng)用的任務(wù)調(diào)度框架,還是一個(gè)可以的候選項(xiàng)目的。
這里不鋪開講,有興趣就去官網(wǎng)看下吧。整體用起來感覺是沒有spring自己的后臺(tái)任務(wù)方便,不過也可以接受,只需要簡(jiǎn)單的配置就可以使用了。
@Scheduled 和@Async的使用
如題,今天在知乎突然看到一份關(guān)于springboot自帶調(diào)度器的問題思考,有這么一段內(nèi)容“在使用@Scheduled注解時(shí),如果不自己重新配置調(diào)度器,那么就會(huì)使用默認(rèn)的,從而會(huì)導(dǎo)致一些調(diào)度執(zhí)行上的問題”;聯(lián)系到自己在程序中使用時(shí)沒有關(guān)注到這個(gè)問題,因此仔細(xì)測(cè)試研究一番,最終了解了其中的一些關(guān)鍵思想。
首先,需要了解@Scheduled 和@Async這倆注解的區(qū)別:
@Scheduled 任務(wù)調(diào)度注解,主要用于配置定時(shí)任務(wù);springboot默認(rèn)的調(diào)度器線程池大小為 1。
@Async 任務(wù)異步執(zhí)行注解,主要用于方法上,表示當(dāng)前方法會(huì)使用新線程異步執(zhí)行;springboot默認(rèn)執(zhí)行器線程池大小為100。
所以,如果在使用springboot定時(shí)器時(shí),如果有多個(gè)定時(shí)任務(wù)時(shí),在使用默認(rèn)的調(diào)度器配置,就會(huì)出現(xiàn)排隊(duì)現(xiàn)象,因?yàn)橥瑫r(shí)只能有一個(gè)任務(wù)在執(zhí)行,這個(gè)時(shí)候當(dāng)一個(gè)任務(wù)掛死,那后面的定時(shí)任務(wù)就不能有效執(zhí)行了;
解決辦法就是自定義調(diào)度器,有兩種方式:
方法一:
@Bean
public TaskScheduler scheduledExecutorService() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("scheduled-thread-");
//設(shè)置線程池關(guān)閉的時(shí)候等待所有任務(wù)都完成再繼續(xù)銷毀其他的Bean
scheduler.setWaitForTasksToCompleteOnShutdown(true);
//設(shè)置線程池中任務(wù)的等待時(shí)間,如果超過這個(gè)時(shí)候還沒有銷毀就強(qiáng)制銷毀,以確保應(yīng)用最后能夠被關(guān)閉,而不是阻塞住
scheduler.setAwaitTerminationSeconds(60);
//這里采用了CallerRunsPolicy策略,當(dāng)線程池沒有處理能力的時(shí)候,該策略會(huì)直接在 execute 方法的調(diào)用線程中運(yùn)行被拒絕的任務(wù);如果執(zhí)行程序已關(guān)閉,則會(huì)丟棄該任務(wù)
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return scheduler;
}
方法二:
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(setExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor setExecutor(){
return Executors.newScheduledThreadPool(10); // 10個(gè)線程來處理。
}
}
上述自定義調(diào)度器的方式,會(huì)有一個(gè)問題:當(dāng)有足夠的空余線程時(shí),多任務(wù)時(shí)并行執(zhí)行,但是同一定時(shí)任務(wù)仍會(huì)同步執(zhí)行(當(dāng)定時(shí)任務(wù)的執(zhí)行時(shí)間大于每次執(zhí)行的時(shí)間間隔時(shí)即可發(fā)現(xiàn));
配合@Async 注解使用,這樣在每次執(zhí)行定時(shí)任務(wù)時(shí)就新開一個(gè)線程,異步非阻塞運(yùn)行;同時(shí)使用這兩個(gè)注解的效果,相當(dāng)于@Scheduled僅僅負(fù)責(zé)調(diào)度,而@Async指定的executor負(fù)責(zé)任務(wù)執(zhí)行,不再使用調(diào)度器中的執(zhí)行器來執(zhí)行任務(wù)(由實(shí)際測(cè)試結(jié)果來猜測(cè)的,并沒有找到對(duì)應(yīng)的源碼邏輯,待后續(xù)補(bǔ)充)。
自定義執(zhí)行器配置如下:
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
// 線程池對(duì)拒絕任務(wù)的處理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot2.x集成JPA快速開發(fā)的示例代碼
這篇文章主要介紹了Spring Boot2.x集成JPA快速開發(fā),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
SpringBoot+Hutool+thymeleaf完成導(dǎo)出Excel的實(shí)現(xiàn)方法
這篇文章主要介紹了SpringBoot+Hutool+thymeleaf完成導(dǎo)出Excel,本篇示例當(dāng)中不僅僅有后端,而且還提供了前端html,html當(dāng)中利用js將后端 輸出流直接下載為文件,需要的朋友可以參考下2022-03-03
SpringBoot后端驗(yàn)證碼的實(shí)現(xiàn)示例
為了防止網(wǎng)站的用戶被通過密碼典爆破,引入驗(yàn)證碼的功能是十分有必要的,本文主要介紹了SpringBoot后端驗(yàn)證碼的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-08-08
解決引用jip-common jar包,報(bào)401 Unauthorized錯(cuò)誤問題
這篇文章主要介紹了解決引用jip-common jar包,報(bào)401 Unauthorized錯(cuò)誤問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
Eolink上傳文件到Java后臺(tái)進(jìn)行處理的示例代碼
這篇文章主要介紹了Eolink上傳文件到Java后臺(tái)進(jìn)行處理,這里是上傳的excel表格數(shù)據(jù)并轉(zhuǎn)換為java集合對(duì)象、然后進(jìn)行業(yè)務(wù)邏輯處理判斷最后保存到數(shù)據(jù)庫(kù)?,需要的朋友可以參考下2022-12-12
springboot下使用shiro自定義filter的個(gè)人經(jīng)驗(yàn)分享
這篇文章主要介紹了springboot下使用shiro自定義filter的個(gè)人經(jīng)驗(yàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
別在Java代碼里亂打日志了,這才是正確的打日志姿勢(shì)
這篇文章主要介紹了別在Java代碼里亂打日志了,這才是正確的打日志姿勢(shì),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06

