通常我們開發的程序都是同步調用的,即程序按照代碼的順序一行一行的逐步往下執行,每一行代碼都必須等待上一行代碼執行完畢才能開始執行。而異步編程則沒有這個限制,代碼的調用不再是阻塞的。所以在一些情景下,通過異步編程可以提高效率,提升接口的吞吐量。這節將介紹如何在Spring Boot中進行異步編程。
開啟異步
新建一個Spring Boot項目,版本為2.1.0.RELEASE,並引入spring-boot-starter-web依賴,項目結構如下所示:
要開啟異步支持,首先得在Spring Boot入口類上加上@EnableAsync注解:
@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
接下來開始編寫異步方法。
在com.example.demo路徑下新建service包,並創建TestService:
@Service
public class TestService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Async
public void asyncMethod() {
sleep();
logger.info("異步方法內部線程名稱:{}", Thread.currentThread().getName());
}
public void syncMethod() {
sleep();
}
private void sleep() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面的Service中包含一個異步方法asyncMethod(開啟異步支持后,只需要在方法上加上@Async注解便是異步方法了)和同步方法syncMethod。sleep方法用於讓當前線程阻塞2秒鍾。
接着在com.example.demo路徑下新建controller包,然后創建TestController:
@RestController
public class TestController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private TestService testService;
@GetMapping("async")
public void testAsync() {
long start = System.currentTimeMillis();
logger.info("異步方法開始");
testService.asyncMethod();
logger.info("異步方法結束");
long end = System.currentTimeMillis();
logger.info("總耗時:{} ms", end - start);
}
@GetMapping("sync")
public void testSync() {
long start = System.currentTimeMillis();
logger.info("同步方法開始");
testService.syncMethod();
logger.info("同步方法結束");
long end = System.currentTimeMillis();
logger.info("總耗時:{} ms", end - start);
}
}
啟動項目,訪問 http://localhost:8080/sync 請求,控制台輸出如下:
可看到默認程序是同步的,由於sleep方法阻塞的原因,testSync方法執行了2秒鍾以上。
訪問 http://localhost:8080/async ,控制台輸出如下:
可看到testAsync方法耗時極少,因為異步的原因,程序並沒有被sleep方法阻塞,這就是異步調用的好處。同時異步方法內部會新啟一個線程來執行,這里線程名稱為task - 1。
默認情況下的異步線程池配置使得線程不能被重用,每次調用異步方法都會新建一個線程,我們可以自己定義異步線程池來優化。
自定義異步線程池
在com.example.demo下新建config包,然后創建AsyncPoolConfig配置類:
@Configuration
public class AsyncPoolConfig {
@Bean
public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(25);
executor.setKeepAliveSeconds(200);
executor.setThreadNamePrefix("asyncThread");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
上面我們通過ThreadPoolTaskExecutor的一些方法自定義了一個線程池,這些方法的含義如下所示:
-
corePoolSize:線程池核心線程的數量,默認值為1(這就是默認情況下的異步線程池配置使得線程不能被重用的原因)。
-
maxPoolSize:線程池維護的線程的最大數量,只有當核心線程都被用完並且緩沖隊列滿后,才會開始申超過請核心線程數的線程,默認值為Integer.MAX_VALUE。
-
queueCapacity:緩沖隊列。
-
keepAliveSeconds:超出核心線程數外的線程在空閑時候的最大存活時間,默認為60秒。
-
threadNamePrefix:線程名前綴。
-
waitForTasksToCompleteOnShutdown:是否等待所有線程執行完畢才關閉線程池,默認值為false。
-
awaitTerminationSeconds:waitForTasksToCompleteOnShutdown的等待的時長,默認值為0,即不等待。
-
rejectedExecutionHandler:當沒有線程可以被使用時的處理策略(拒絕任務),默認策略為abortPolicy,包含下面四種策略:
-
callerRunsPolicy:用於被拒絕任務的處理程序,它直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。
-
abortPolicy:直接拋出java.util.concurrent.RejectedExecutionException異常。
-
discardOldestPolicy:當線程池中的數量等於最大線程數時、拋棄線程池中最后一個要執行的任務,並執行新傳入的任務。
-
discardPolicy:當線程池中的數量等於最大線程數時,不做任何動作。
-
要使用該線程池,只需要在@Async注解上指定線程池Bean名稱即可:
@Service
public class TestService {
......
@Async("asyncThreadPoolTaskExecutor")
public void asyncMethod() {
......
}
......
}
重啟項目,再次訪問 http://localhost:8080/async ,控制台輸出入下:
處理異步回調
如果異步方法具有返回值的話,需要使用Future來接收回調值。我們修改TestService的asyncMethod方法,給其添加返回值:
@Async("asyncThreadPoolTaskExecutor")
public Future<String> asyncMethod() {
sleep();
logger.info("異步方法內部線程名稱:{}", Thread.currentThread().getName());
return new AsyncResult<>("hello async");
}
泛型指定返回值的類型,AsyncResult為Spring實現的Future實現類:
接着改造TestController的testAsync方法:
@GetMapping("async")
public String testAsync() throws Exception {
long start = System.currentTimeMillis();
logger.info("異步方法開始");
Future<String> stringFuture = testService.asyncMethod();
String result = stringFuture.get();
logger.info("異步方法返回值:{}", result);
logger.info("異步方法結束");
long end = System.currentTimeMillis();
logger.info("總耗時:{} ms", end - start);
return stringFuture.get();
}
Future接口的get方法用於獲取異步調用的返回值。
重啟項目,訪問 http://localhost:8080/async 控制台輸出如下所示:
通過返回結果我們可以看出Future的get方法為阻塞方法,只有當異步方法返回內容了,程序才會繼續往下執行。get還有一個get(long timeout, TimeUnit unit)重載方法,我們可以通過這個重載方法設置超時時間,即異步方法在設定時間內沒有返回值的話,直接拋出java.util.concurrent.TimeoutException異常。
比如設置超時時間為60秒:
String result = stringFuture.get(60, TimeUnit.SECONDS);