1 基礎(chǔ)
Java提供的Thread需要寫一堆的代碼,用了spring,想讓哪個方法是異步的,加個注解,就搞定了。
spring AOP,會主動攔截添加注解@Scheduled/@Async的方法,然后,由spring維護線程的整個生命周期。
- 定時任務(wù)@Schedule
@Scheduled(cron = "0/20 * * * * ?") //每20秒執(zhí)行一次
- 異步任務(wù)@Async
釋義:開啟異步任務(wù),執(zhí)行標記的方法。方法看起來還是普通的方法,但是呢,調(diào)用的時候,spring會主動開啟新的線程。
思考:如果,開啟的線程數(shù)量太多,服務(wù)器處理不完,怎么辦?
答:類似jdbc的Connection。同樣的道理,創(chuàng)建一個線程池,需要線程的時候,從池子里租出來,用完再放回去;得不到服務(wù)的線程,不能直接丟棄,還需要再維護一個任務(wù)隊列;如果排隊的任務(wù)超出了隊列的范圍,那就得考慮擴容了,調(diào)參、或是多加臺服務(wù)器、或者直接拒絕。
2 進階:共用一個Thread Pool
| method上添加的注解 | Thread Pool class | configuaration |
|---|---|---|
| @Schedule | ThreadPoolTaskScheduler | @EnableScheduling |
| @Async | ThreadPoolTaskExecutor | @EnableAsync |
2.1 配置bean
@Configuration //添加這個注解的class,只在容器啟動的時候加載一次
@EnableAsync //啟用異步任務(wù):TaskExecutor
@EnableScheduling //啟用定時任務(wù):TaskScheduler
public class CommonBeanConfigure {
@Bean("asyncExecutor") //指定TaskExecutor 的bean name
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(20);
threadPoolTaskExecutor.setQueueCapacity(1000);
threadPoolTaskExecutor.setThreadNamePrefix("async-t-");
return threadPoolTaskExecutor;
}
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("p-scheduler-");
scheduler.setPoolSize(10);
return scheduler;
}
}
2.2 配置需要開啟多線程的method
@Async("asyncExecutor") //指定TaskExecutor 的bean name
public void refreshOnlineUser(OAuth2Authentication auth2Authentication){...}
@Scheduled(cron = "0/20 * * * * ?") //每隔20秒執(zhí)行一次
public void count() {...}
2.3 測試
請注意“ThreadNamePrefix”,如果打印出來的日志里,thread name沒有預(yù)設(shè)的前綴,那么,配置的這個bean TaskExecutor 或 TaskScheduler 就沒有生效(這兩個線程的前綴不同)
此時,請檢查@Async、@Scheduled。不需要對比所有的配置文件,最快的解決方案:
- 配置TaskExecutor 或 TaskScheduler 時,設(shè)置bean name;
- 同時,指定@Async、@Scheduled使用的bean name
3 異常處理
異步任務(wù)的異常,不會讓主線程停止運行。(應(yīng)該的,本來就不在一個線程里)
當然,凡事都有例外,比如,分布式事務(wù)的“其中一種機制”補償機制,與這種情況類似。當異步任務(wù)拋異常后,通知main Thread使用“補償措施”回滾事務(wù)。
請參考http://blog.csdn.net/blueheart20/article/details/44648667
目前,還沒有這樣的需求,所以,不建議使用這種“重型”的解決方案
4 補充描述
4.1 線程的名字沒有預(yù)設(shè)的前綴
首先,能看到線程的名字,說明在方法上添加的注解生效了;其次,預(yù)定的前綴沒有打印出來,那就說明bean的配置,沒有生效。
一句話,spring根據(jù)方法上的注解,使用默認的實現(xiàn)類創(chuàng)建了線程,沒有使用配置的Thread pool。
解決方案:
- 設(shè)置bean的name。eg.
@Bean("asyncTask")
public TaskExecutor taskExecutor()
- 指定方法使用的bean name。 eg.
@Async("asyncTask")
public void hello()
4.2 什么樣的class、method可以使用@Async、@Scheduled
開啟異步任務(wù)、定時任務(wù),只跟method有關(guān)。每次調(diào)用這些method的時候,spring上下文都會開啟新的線程。
所以,任何一個method都可以添加@Async、@Scheduled。
注意:這兩個注解必須放在實現(xiàn)類的方法上,如果,放在interface的方法聲明上,不會生效的。
錯誤示例:
public interface UserLoginHistoryService {
//每晚12點清理日志 --- 這個定時任務(wù)不會執(zhí)行的,因為,標記在interface的方法聲明上了
@Scheduled(cron = "0 0 0 * * ?")
void deleteHistory();
}
4.3 為什么必須指定TaskExecutor的bean name?
網(wǎng)上可以找到很多帖子,說是@Async沒有生效。據(jù)說是有其他的配置覆蓋了(沒找到...)。所以,我猜測,應(yīng)該是spring的BUG。
解決辦法:指定TaskExecutor 的name
4.4 @EnableAsync、@EnableScheduling
這兩個注解是用來通知spring 容器,嘗試加載相關(guān)的bean,啟用異步任務(wù)或定時任務(wù)。所以,一個項目里,只需要在任意一個@Configuaration標記的class上,添加這兩個注解即可。
當然,規(guī)范些,還是在配置bean TaskExecutor 、TaskScheduler 的class上,添加@EnableAsync 、@EnableScheduling
注:不能濫用注解。雖然,多次配置,也不會報錯,但,多余的配置,會造成誤解。
4.5 當預(yù)設(shè)的線程用完了
線程池中配置的線程是有數(shù)的,當用完了,程序或者是等待,或者,不處理。死活都要撐著,只能讓服務(wù)器崩潰。這種情況,主要針對的是TaskExecutor (通常,一個項目中TaskScheduler 定時任務(wù)是有限的)。
1、允許等待的線程,本身處理的任務(wù),耗時要少些。再配合QueueCapacity隊列,可以最大限度地保障系統(tǒng)高效地運行(能處理的任務(wù),快速處理完,然后,歸還線程;不能處理的任務(wù),先排隊,輪到了,再處理);
2、直接拒絕的線程,應(yīng)該是那些本身就要耗時很長,超出服務(wù)器處理能力的請求?;蛘呔芙^,或者,多加幾臺服務(wù)器
當然,也可以適當增加線程的數(shù)量,這個,得考慮硬件條件
4.6 守護線程
web應(yīng)用跑在JVM上,線程也跑在JVM上,嚴格說起來,咱們new Thread()跟web應(yīng)用沒關(guān)系。換句話說,關(guān)閉web應(yīng)用后,某些線程還在繼續(xù)運行。
如果你使用我的配置方案啟用Async/Schedule,spring容器會自動管理這些線程。當web應(yīng)用關(guān)閉后,相關(guān)的線程也會stop。
詳細的原理,請查閱“守護線程”