很多業務場景需要使用異步去完成,比如:發送短信通知。要完成異步操作一般有兩種:
1、消息隊列MQ
2、線程池處理。
我們來看看Spring框架中如何去使用線程池來完成異步操作,以及分析背後的原理。
一. Spring異步線程池的接口類 :TaskExecutor
在Spring4中,Spring中引入了一個新的註解@Async,這個註解讓我們在使用Spring完成異步操作變得非常方便。
Spring異步線程池的接口類,其實質是java.util.concurrent.Executor
Spring 已經實現的異常線程池:
1. SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調用都會創建一個新的線程。
2. SyncTaskExecutor:這個類沒有實現異步調用,只是一個同步操作。只適用於不需要多線程的地方
3. ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類
4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的類。線程池同時被quartz和非quartz使用,才需要使用此類
5. ThreadPoolTaskExecutor :最常使用,推薦。 其實質是對java.util.concurrent.ThreadPoolExecutor的包裝,
關於java-多線程和線程池:https://guisu.blog.csdn.net/article/details/7945539
我們查看ThreadPoolExecutor初始化的源碼就知道使用ThreadPoolExecutor。
二、簡單使用說明
Spring中用@Async註解標記的方法,稱爲異步方法。在spring boot應用中使用@Async很簡單:
1、調用異步方法類上或者啓動類加上註解@EnableAsync
2、在需要被異步調用的方法外加上@Async
3、所使用的@Async註解方法的類對象應該是Spring容器管理的bean對象;
啓動類加上註解@EnableAsync:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class CollectorApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(CollectorApplication.class, args);
}
}
在需要被異步調用的方法外加上@Async,同時類AsyncService加上註解@Service或者@Component,使其對象成爲Spring容器管理的bean對象;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class AsyncService {
@Async
public void asyncMethod(String s) {
System.out.println("receive:" + s);
}
public void test() {
System.out.println("test");
asyncMethod();//同一個類裏面調用異步方法
}
@Async
public void test2() {
AsyncService asyncService = context.getBean(AsyncService.class);
asyncService.asyncMethod();//異步
}
/**
* 異布調用返回Future
*/
@Async
public Future<String> asyncInvokeReturnFuture(int i) {
System.out.println("asyncInvokeReturnFuture, parementer="+ i);
Future<String> future;
try {
Thread.sleep(1000 * 1);
future = new AsyncResult<String>("success:" + i);
} catch (InterruptedException e) {
future = new AsyncResult<String>("error");
}
return future;
}
}
//異步方法和普通的方法調用相同
asyncService.asyncMethod("123");
Future<String> future = asyncService.asyncInvokeReturnFuture(100);
System.out.println(future.get());
如果將一個類聲明爲異步類@Async,那麼這個類對外暴露的方法全部成爲異步方法。
@Async
@Service
public class AsyncClass {
public AsyncClass() {
System.out.println("----init AsyncClass----");
}
volatile int index = 0;
public void foo() {
System.out.println("asyncclass foo, index:" + index);
}
public void foo(int i) {
this.index = i;
System.out.println("asyncclass foo, index:" + i);
}
public void bar(int i) {
this.index = i;
System.out.println("asyncclass bar, index:" + i);
}
}
這裏需要注意的是:
1、同一個類裏面調用異步方法不生效:原因默認類內的方法調用不會被aop攔截,即調用方和被調用方是在同一個類中,是無法產生切面的,該對象沒有被Spring容器管理。即@Async方法不生效。
解決辦法:如果要使同一個類中的方法之間調用也被攔截,需要使用spring容器中的實例對象,而不是使用默認的this,因爲通過bean實例的調用纔會被spring的aop攔截
本例使用方法:AsyncService asyncService = context.getBean(AsyncService.class); 然後使用這個引用調用本地的方法即可達到被攔截的目的
備註:這種方法只能攔截protected,default,public方法,private方法無法攔截。這個是spring aop的一個機制。
2、如果不自定義異步方法的線程池默認使用SimpleAsyncTaskExecutor。SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調用都會創建一個新的線程。併發大的時候會產生嚴重的性能問題。
3、異步方法返回類型只能有兩種:void和java.util.concurrent.Future。
1)當返回類型爲void的時候,方法調用過程產生的異常不會拋到調用者層面,
可以通過注AsyncUncaughtExceptionHandler來捕獲此類異常
2)當返回類型爲Future的時候,方法調用過程產生的異常會拋到調用者層面
三、定義通用線程池
1、定義線程池
在Spring Boot主類中定義一個線程池,public Executor taskExecutor() 方法用於自定義自己的線程池,線程池前綴”taskExecutor-”。如果不定義,則使用系統默認的線程池。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean
public Executor taskExecutor1() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(5); //線程池活躍的線程數
pool.setMaxPoolSize(10); //線程池最大活躍的線程數
pool.setWaitForTasksToCompleteOnShutdown(true);
pool.setThreadNamePrefix("defaultExecutor");
return pool;
}
@Bean("taskExecutor")
public Executor taskExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
}
}
上面我們通過ThreadPoolTaskExecutor
創建了一個線程池,同時設置瞭如下參數:
- 核心線程數10:線程池創建時初始化的線程數
- 最大線程數20:線程池最大的線程數,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
- 緩衝隊列200:用來緩衝執行任務的隊列
- 允許線程的空閒時間60秒:超過了核心線程數之外的線程,在空閒時間到達之後會被銷燬
- 線程池名的前綴:設置好了之後可以方便我們定位處理任務所在的線程池
- 線程池對拒絕任務的處理策略:此處採用了
CallerRunsPolicy
策略,當線程池沒有處理能力的時候,該策略會直接在execute
方法的調用線程中運行被拒絕的任務;如果執行程序已被關閉,則會丟棄該任務 - 設置線程池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean
- 設置線程池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住
也可以單獨類來配置線程池:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Created by huangguisu on 2020/6/10.
*/
@Configuration
@EnableAsync
public class MyThreadPoolConfig {
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 20;
private static final int QUEUE_CAPACITY = 200;
public static final String BEAN_EXECUTOR = "bean_executor";
/**
* 事件和情感接口線程池執行器配置
* @return 事件和情感接口線程池執行器bean
*
*/
@Bean(BEAN_EXECUTOR)
public Executor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
// 設置隊列容量
executor.setQueueCapacity(QUEUE_CAPACITY);
// 設置線程活躍時間(秒)
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("SE-Pool#Task");
// 設置拒絕策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
同時注意需要在配置類上添加@EnableAsync
,當然也可以在啓動類上添加,表示開啓spring的@@Async
2、異步方法使用線程池
只需要在@Async
註解中指定線程池名即可
@Component
public class Task {
//默認使用線程池
@Async
public void doTaskOne() throws Exception {
System.out.println("開始做任務");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務耗時:" + (end - start) + "毫秒");
}
//根據Bean Name指定特定線程池
@Async("taskExecutor")
public void doTaskOne() throws Exception {
System.out.println("開始做任務");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務耗時:" + (end - start) + "毫秒");
}
}
3、通過xml配置定義線程池
Bean文件配置: spring_async.xml
1. 線程的前綴爲xmlExecutor
2. 啓動異步線程池配置
<!-- 等價於 @EnableAsync, executor指定線程池 -->
<task:annotation-driven executor="xmlExecutor"/>
<!-- id指定線程池產生線程名稱的前綴 -->
<task:executor
id="xmlExecutor"
pool-size="5-25"
queue-capacity="100"
keep-alive="120"
rejection-policy="CALLER_RUNS"/>
啓動類導入xml文件:
@SpringBootApplication
@ImportResource("classpath:/async/spring_async.xml")
public class AsyncApplicationWithXML {
private static final Logger log = LoggerFactory.getLogger(AsyncApplicationWithXML.class);
public static void main(String[] args) {
log.info("Start AsyncApplication.. ");
SpringApplication.run(AsyncApplicationWithXML.class, args);
}
}
線程池參數說明
1. ‘id’ : 線程的名稱的前綴
2. ‘pool-size’:線程池的大小。支持範圍”min-max”和固定值(此時線程池core和max sizes相同)
3. ‘queue-capacity’ :排隊隊列長度
4. ‘rejection-policy’: 對拒絕的任務處理策略
5. ‘keep-alive’ : 線程保活時間(單位秒)
四、異常處理
上面也提到:在調用方法時,可能出現方法中拋出異常的情況。在異步中主要有有兩種異常處理方法:
1. 對於方法返回值是Futrue的異步方法:
a) 、一種是在調用future的get時捕獲異常;
b)、 在異常方法中直接捕獲異常
2. 對於返回值是void的異步方法:通過AsyncUncaughtExceptionHandler處理異常
@Component
public class AsyncException {
/**
* 帶參數的異步調用 異步方法可以傳入參數
* 對於返回值是void,異常會被AsyncUncaughtExceptionHandler處理掉
* @param s
*/
@Async
public void asyncInvokeWithException(String s) {
log.info("asyncInvokeWithParameter, parementer={}", s);
throw new IllegalArgumentException(s);
}
/**
* 異常調用返回Future
* 對於返回值是Future,不會被AsyncUncaughtExceptionHandler處理,需要我們在方法中捕獲異常並處理
* 或者在調用方在調用Futrue.get時捕獲異常進行處理
*
* @param i
* @return
*/
@Async
public Future<String> asyncInvokeReturnFuture(int i) {
System.out.println("asyncInvokeReturnFuture, parementer={}", i);
Future<String> future;
try {
Thread.sleep(1000 * 1);
future = new AsyncResult<String>("success:" + i);
throw new IllegalArgumentException("a");
} catch (InterruptedException e) {
future = new AsyncResult<String>("error");
} catch(IllegalArgumentException e){
future = new AsyncResult<String>("error-IllegalArgumentException");
}
return future;
}
}
實現AsyncConfigurer接口對異常線程池更加細粒度的控制
a) 創建線程自己的線程池
b) 對void方法拋出的異常處理的類AsyncUncaughtExceptionHandler
@Service
public class MyAsyncConfigurer implements AsyncConfigurer{
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
threadPool.setCorePoolSize(1);
threadPool.setMaxPoolSize(1);
threadPool.setWaitForTasksToCompleteOnShutdown(true);
threadPool.setAwaitTerminationSeconds(60 * 15);
threadPool.setThreadNamePrefix("MyAsync-");
threadPool.initialize();
return threadPool;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncExceptionHandler();
}
/**
* 自定義異常處理類
*/
class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
System.out.println("Exception message - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : obj) {
System.out.println("Parameter value - " + param);
}
}
}
}
五、問題
上面也提到:如果不自定義異步方法的線程池默認使用SimpleAsyncTaskExecutor。SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調用都會創建一個新的線程。併發大的時候會產生嚴重的性能問題。
一般的錯誤OOM:OutOfMemoryError:unable to create new native thread,創建線程數量太多,佔用內存過大.
解決辦法:一般最好使用自定義線程池,做一些特殊策略, 比如自定義拒絕策略,如果隊列滿了,則拒絕處理該任務。