如果我們希望某段代碼異步執行的話,通常是創建一個繼承或實現線程相關的類或接口來實現的,具體過程不在本文贅述。但是如果我們所有的代碼都是創建線程去執行的話,就會遇到線程數不可控等諸多問題,於是我們引入了線程池的概念,相關討論可參考《十個爲什麼》之四:爲什麼要有線程池?。
即使有了線程池,對於異步調用的代碼我們還是需要每次都爲它創建線程類,這個麻煩事在 Java8 中通過 lambda 表達式可以省略重複代碼。其實,對於這樣的需求,Spring 也提供瞭解決方案:@EnableAsync 和 @Async 註解,其中,@EnableAsyn 註解寫在啓動類上就行了,它表示本項目啓用了異步調用的功能。如下圖所示:
這樣,就再也不需要自己手動創建線程池來實現異步調用了,而異步的方法也不需要再主動實現Runnable之類的接口了,而只需要把 @Async 註解寫在你希望異步調用的方法上就行,如下圖所示:
凡是加上了 @Async 註解的方法,被調用後它都是異步執行的。
1. TaskExecutor
這種方式確實非常方便,但是我們還是需要注意一下線程池的問題,因爲到目前爲止,被 @Async 註解的方法,每次執行都會創建一個線程去執行的。Spring 需要我們去配置一個叫做 TaskExecutor 的東西。
默認情況下,Spring 將搜索相關的線程池定義:要麼在上下文中搜索唯一的 TaskExecutor bean,要麼搜索名爲 "taskExecutor" 的 Executor bean。如果兩者都無法解析,則將使用 SimpleAsyncTaskExecutor 來處理異步方法調用。
通常,應該定義一個 ThreadPoolTaskExecutor 來使用線程池,使得異步調用的多線程是可控的。
Spring 已經實現的異步線程池有以下5個:
SimpleAsyncTaskExecutor
作用:不是真的線程池,這個類不重用線程,每次調用都會創建一個新的線程
適用性:默認值、不推薦;這是沒有配置 TaskExecutor 時,Spring 的默認選擇
SyncTaskExecutor
作用: 這個類沒有實現異步調用,任務是同步進行的;
適用性:不需要多線程的地方
ConcurrentTaskExecutor
作用:Executor 的適配類
適用性:不推薦;ThreadPoolTaskExecutor 不滿足要求才用考慮使用這個類
SimpleThreadPoolTaskExecutor
作用:是 Quartz 的 SimpleThreadPool 的類;
適用性:線程池同時被 quartz 和非 quartz 使用,才需要使用此類
ThreadPoolTaskExecutor
作用:實質上是對 java.util.concurrent.ThreadPoolExecutor 的封裝
適用性:最常使用,推薦
此外,我在《十個爲什麼》之四:爲什麼要有線程池?一文中討論了線程池的作用以及相關參數的意義,其中還討論了一個問題:一個項目中是否需要使用統一的線程池去管理所有的多線程操作呢?我認爲沒有這個必要,因爲 Java 中使用線程池聲明的線程,在沒有實際使用的時候,是幾乎不會消耗系統資源的。基於上述原因,我們可以針對不同的場景創建不同的線程池。但如果項目中都使用了上述做法,那麼等於只有一個公用的線程池了——或許使用同一個線程池並不是什麼問題,只要我們爲此設計一個更加通用的線程池即可。
2. @Async
註解 @Async 下的方法是一個異步執行的方法
異步的方法有3種
- 最簡單的異步調用,返回值爲void
- 帶參數的異步調用 異步方法可以傳入參數
- 異常調用返回Future
如下方式會使 @Async 失效
-
異步方法使用 static 修飾,失效
-
異步類沒有使用 @Component 註解(或其他註解)導致 Spring 無法掃描到異步類,失效
-
類的對象需要使用 @Autowired 或 @Resource 等註解自動注入,不能自己手動 new 對象,否則失效
-
如果使用 SpringBoot 框架必須在啓動類中增加 @EnableAsync 註解,或者把該註解放在某個配置類中,否則失效
-
在 Async 方法上標註 @Transactional 是沒用的。 在 Async 方法調用的方法上標註 @Transactional 有效,但該事務註解不會控制到異步方法中的代碼
-
調用被 @Async 標記的方法的調用者不能和被調用的方法在同一類中不然不會起作用
3. 開啓異步的配置類
下面是一個配置的代碼 demo:
@Configuration
@EnableAsync // 啓動異步調用
public class ThreadPoolTaskConfig {
@Bean("taskExecutor") // bean的名稱,默認爲首字母小寫的方法名
public ThreadPoolTaskExecutor getAsyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("Async-Service-"); // 線程池名前綴
executor.setCorePoolSize(10); // 核心線程數(默認線程數)
executor.setMaxPoolSize(100); // 最大線程數
executor.setQueueCapacity(200); // 緩衝隊列數
executor.setKeepAliveSeconds(10); // 允許線程空閒時間(單位:秒)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 線程池對拒絕任務的處理策略
// 初始化
executor.initialize();
return executor;
}
}
總結
以上便是 Spring 的 異步調用 @Async 的用法,使用方式並沒有難度,唯一需要注意的是線程池的配置。雖然這樣的使用方式確實省了不少事情,但是基於更多的自由因素的考慮,我個人還是建議使用線程池而不是這種註解的方式,而且線程池應該在不同的場景給出不同的配置,而不是使用一個”大而通用“的線程池配置。