對於異步方法調用,從Spring3開始提供了@Async註解,該註解可以被標註在方法上,以便異步地調用該方法。調用者將在調用時立即返回,方法的實際執行將提交給Spring TaskExecutor的任務中,由指定的線程池中的線程執行。
在實際項目中, 使用@Async調用線程池,推薦等方式是是使用自定義線程池的模式,自定義線程池常用的方案:重新實現AsyncConfigurer接口。
場景
- 同步: 同步就是整個處理過程順序執行,當各個過程都執行完畢,並返回結果。
- 異步: 異步調用則是隻是發送了調用的指令,調用者無需等待被調用的方法完全執行完畢;而是繼續執行下面的流程。例如, 在某個調用中,需要順序調用 A, B, C三個過程方法;如他們都是同步調用,則需要將他們都順序執行完畢之後,方算作過程執行完畢;如B爲一個異步的調用方法,則在執行完A之後,調用B,並不等待B完成,而是執行開始調用C,待C執行完畢之後,就意味着這個過程執行完畢了。在Java中,一般在處理類似的場景之時,都是基於創建獨立的線程去完成相應的異步調用邏輯,通過主線程和不同的業務子線程之間的執行流程,從而在啓動獨立的線程之後,主線程繼續執行而不會產生停滯等待的情況。
Spring 已經實現的線程池
- SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,默認每次調用都會創建一個新的線程
- SyncTaskExecutor:這個類沒有實現異步調用,只是一個同步操作。只適用於不需要多線程的地方。
- ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類。
- SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的類。線程池同時被quartz和非quartz使用,才需要使用此類。
- ThreadPoolTaskExecutor :最常使用,推薦。其實質是對java.util.concurrent.ThreadPoolExecutor的包裝。
常見的異步方式有:
- 最簡單的異步調用,返回值爲void。
- 帶參數的異步調用,異步方法可以傳入參數。
- 存在返回值,常調用返回Future/CompletableFuture。
@Async應用默認線程池
Spring應用默認的線程池,指在@Async註解在使用時,不指定線程池的名稱。查看源碼,@Async的默認線程池爲SimpleAsyncTaskExecutor。
無返回值的異步調用
@Override
@Async("taskExecutor")
public void pageExportOrderBigExcel(HttpServletResponse response, JSONObject queryConditionDataJson, SdSchoolFilterConfig sdSchoolFilterConfig, LoginUser loginUser, SdSchoolDataExportTaskRecord exportTask, HttpServletRequest request, String tenantId) {
try {
Thread.sleep(1000);
exportTask.setExportTaskStartTime(new Date());
sdSchoolOrderService.exportOrderBigExcelPage(response, queryConditionDataJson, exportTask, sdSchoolFilterConfig.getFilterName(), loginUser, request, tenantId);
exportTask.setExportTaskEndTime(new Date());
exportTaskRecordService.updateById(exportTask);
} catch (Exception e) {
log.error("訂單數據分頁導出失敗", e);
}
}
默認線程池的弊端
在線程池應用中,參考阿里巴巴java開發規範:線程池不允許使用Executors去創建,不允許使用系統默認的線程池,推薦通過ThreadPoolExecutor的方式,這樣的處理方式讓開發的工程師更加明確線程池的運行規則,規避資源耗盡的風險。Executors各個方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor:主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool:要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。
@Async默認異步配置使用的是SimpleAsyncTaskExecutor,該線程池默認來一個任務創建一個線程,若系統中不斷的創建線程,最終會導致系統佔用內存過高,引發OutOfMemoryError錯誤。針對線程創建問題,SimpleAsyncTaskExecutor提供了限流機制,通過concurrencyLimit屬性來控制開關,當concurrencyLimit>=0時開啓限流機制,默認關閉限流機制即concurrencyLimit=-1,當關閉情況下,會不斷創建新的線程來處理任務。基於默認配置,SimpleAsyncTaskExecutor並不是嚴格意義的線程池,達不到線程複用的功能。
@Async應用自定義線程池
自定義線程池,可對系統中線程池更加細粒度的控制,方便調整線程池大小配置,線程執行異常控制和處理。在設置系統自定義線程池代替默認線程池時,雖可通過多種模式設置,但替換默認線程池最終產生的線程池有且只能設置一個(不能設置多個類繼承AsyncConfigurer)。自定義線程池有如下方式:
- 重新實現接口AsyncConfigurer;
- 繼承AsyncConfigurerSupport;
- 配置由自定義的TaskExecutor替代內置的任務執行器。
通過查看Spring源碼關於@Async的默認調用規則,會優先查詢源碼中實現AsyncConfigurer這個接口的類,實現這個接口的類爲AsyncConfigurerSupport。但默認配置的線程池和異步處理方法均爲空,所以,無論是繼承或者重新實現接口,都需指定一個線程池。且重新實現 public Executor getAsyncExecutor()方法。
實現接口AsyncConfigurer
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = 10;
executor.setCorePoolSize(corePoolSize);
int maxPoolSize = 50;
executor.setMaxPoolSize(maxPoolSize);
int queueCapacity = 10;
executor.setQueueCapacity(queueCapacity);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix( "asyncServiceExecutor-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return executor();
}
}
繼承AsyncConfigurerSupport
Configuration
@EnableAsync
class SpringAsyncConfigurer extends AsyncConfigurerSupport {
@Bean
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
threadPool.setCorePoolSize(3);
threadPool.setMaxPoolSize(3);
threadPool.setWaitForTasksToCompleteOnShutdown(true);
threadPool.setAwaitTerminationSeconds(60 * 15);
return threadPool;
}
@Override
public Executor getAsyncExecutor() {
return asyncExecutor;
}
}
配置自定義的TaskExecutor(建議採用方式)
/**
* 線程池參數配置,多個線程池實現線程池隔離,@Async註解,默認使用系統自定義線程池,可在項目中設置多個線程池,在異步調用的時候,指明需要調用的線程池名稱,比如:@Async("taskName")
*
* @author: jacklin
* @since: 2021/5/18 11:44
**/
@EnableAsync
@Configuration
public class TaskPoolConfig {
/**
* 異步導出
*
* @author: jacklin
* @since: 2021/11/16 17:41
**/
@Bean("taskExecutor")
public Executor taskExecutor() {
//返回可用處理器的Java虛擬機的數量 12
int i = Runtime.getRuntime().availableProcessors();
System.out.println("系統最大線程數 : " + i);
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心線程池大小
executor.setCorePoolSize(16);
//最大線程數
executor.setMaxPoolSize(20);
//配置隊列容量,默認值爲Integer.MAX_VALUE
executor.setQueueCapacity(99999);
//活躍時間
executor.setKeepAliveSeconds(60);
//線程名字前綴
executor.setThreadNamePrefix("asyncServiceExecutor -");
//設置此執行程序應該在關閉時阻止的最大秒數,以便在容器的其餘部分繼續關閉之前等待剩餘的任務完成他們的執行
executor.setAwaitTerminationSeconds(60);
//等待所有的任務結束後再關閉線程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
多個線程池
@Async註解,使用系統默認或者自定義的線程池(代替默認線程池)。可在項目中設置多個線程池,在異步調用時,指明需要調用的線程池名稱,如@Async("new_taskName")。