javascript
SpringBoot利用@Async注解实现异步调用
前言:異步編程是讓程序并發(fā)運行的一種手段,使用異步編程可以大大提高我們程序的吞吐量,減少用戶的等待時間。在Java并發(fā)編程中實現(xiàn)異步功能,一般是需要使用線程或者線程池。而實現(xiàn)一個線程,要么繼承Thread類,要么實現(xiàn)Runnable接口,然后在run方法中寫具體的業(yè)務邏輯代碼。開發(fā)Spring的大神們,為了簡化這類異步操作,已經(jīng)幫我們把異步功能封裝好了。Spring中提供了@Async注解,我們可以通過它即可開啟異步功能,使用起來非常方便。
一、異步編程
異步編程允許多個事情同時發(fā)生,當程序調(diào)用需要長時間運行的方法時,它不會阻塞當前的執(zhí)行流程,程序可以繼續(xù)運行,當方法執(zhí)行完成時通知給主線程根據(jù)需要獲取其執(zhí)行結果或者失敗異常的原因。使用異步編程可以大大提高我們程序的吞吐量,可以更好的面對更高的并發(fā)場景并更好的利用現(xiàn)有的系統(tǒng)資源,同時也會一定程度上減少用戶的等待時間等。
1.1、什么是異步調(diào)用?
異步調(diào)用是相對于同步調(diào)用而言的,同步調(diào)用是指程序按預定順序一步步執(zhí)行,每一步必須等到上一步執(zhí)行完后才能執(zhí)行,異步調(diào)用則無需等待上一步程序執(zhí)行完即可執(zhí)行。異步調(diào)用可以減少程序執(zhí)行時間。
?
1.2、如何實現(xiàn)異步調(diào)用?
多線程,這是很多人第一眼想到的關鍵詞,沒錯,多線程就是一種實現(xiàn)異步調(diào)用的方式。
-
在非spring目項目中我們要實現(xiàn)異步調(diào)用的就是使用多線程方式,可以自己實現(xiàn)Runable接口或者集成Thread類,或者使用jdk1.5以上提供了的Executors線程池;
-
從Spring3開始提供了@Async注解,用于標注某個方法或某個類里面的所有方法都是需要異步處理的。被注解的方法被調(diào)用的時候,會在新線程中執(zhí)行,而調(diào)用它的方法會在原來的線程中執(zhí)行。這樣可以避免阻塞、以及保證任務的實時性。適用于處理log、發(fā)送郵件、短信……等。
二、Java 實現(xiàn)異步編程的幾種方式
2.1、非Spring項目利用多線程
①直接new線程
在 Java 語言中最簡單使用異步編程的方式就是創(chuàng)建一個 Thread 來實現(xiàn),如果你使用的 JDK 版本是 8 以上的話,可以使用 Lambda 表達式會更加簡潔。
Thread t = new Thread() {@Overridepublic void run() {longTimeMethod();} }; t.start();但是new Thread()只能作為示例使用,如果用到了生產(chǎn)環(huán)境發(fā)生事故后果自負,使用上面這種 Thread 方式異步編程存在兩個明顯的問題。
-
創(chuàng)建線程沒有復用。我們知道頻繁的線程創(chuàng)建與銷毀是需要一部分開銷的,而且示例里也沒有限制線程的個數(shù),如果使用不當可能會把系統(tǒng)線程用盡,從而引發(fā)事故,這個問題使用線程池可以解決。
-
異步任務無法獲取最終的執(zhí)行結果,可以使用FutureTask 的方式。
②使用線程池
private ExecutorService executor = Executors.newCachedThreadPool() ;public void fun() throws Exception {executor.submit(new Runnable(){@overridepublic void run() {try {//要執(zhí)行的業(yè)務代碼,我們這里沒有寫方法,可以讓線程休息幾秒進行測試Thread.sleep(10000);System.out.print("睡夠啦~");}catch(Exception e) {throw new RuntimeException("報錯啦!!");}}});}Executors是concurrent包下的一個類,為我們提供了創(chuàng)建線程池的簡便方法。
Executors可以創(chuàng)建我們常用的四種線程池:
(1)newCachedThreadPool 創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。不設上限,提交的任務將立即執(zhí)行。
(2)newFixedThreadPool 創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待。
(3)newScheduledThreadPool 創(chuàng)建一個定長線程池,支持定時及周期性任務執(zhí)行。
(4)newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池執(zhí)行任務。
submit方法
線程池建立完畢之后,我們就需要往線程池提交任務。通過線程池的submit方法即可,submit方法接收兩種Runable和Callable。
區(qū)別如下:
Runable是實現(xiàn)該接口的run方法,callable是實現(xiàn)接口的call方法。
callable允許使用返回值,callable允許拋出異常。
2.2、Spring項目異步任務處理,@Async的配置和使用
從Spring3開始提供了@Async注解用于異步方法調(diào)用,該注解可以被標注在方法上,以便異步地調(diào)用該方法。調(diào)用者將在調(diào)用時立即返回,方法的實際執(zhí)行將提交給Spring TaskExecutor的任務中,由指定的線程池中的線程執(zhí)行。
-
@Async應用默認線程池,指在@Async注解在使用時,不指定線程池的名稱。查看源碼,@Async的默認線程池為SimpleAsyncTaskExecutor。
-
@Async默認異步配置使用的是SimpleAsyncTaskExecutor,該線程池默認來一個任務創(chuàng)建一個線程,若系統(tǒng)中不斷的創(chuàng)建線程,最終會導致系統(tǒng)占用內(nèi)存過高,引發(fā)OutOfMemoryError錯誤。基于默認配置,SimpleAsyncTaskExecutor并不是嚴格意義的線程池,達不到線程復用的功能。
-
在項目應用中,@Async調(diào)用線程池,推薦使用自定義線程池的模式。自定義線程池常用方案:重新實現(xiàn)接口AsyncConfigurer。
?三、SpringBoot利用@Async注解實現(xiàn)異步調(diào)用
使用@Async注解開啟的異步功能,默認情況下,每次都會創(chuàng)建一個新線程。如果在高并發(fā)的場景下,可能會產(chǎn)生大量的線程,從而導致OOM問題。所以,大家在使用@Async注解的異步功能時,請別忘了自定義一個線程池。
3.1、新建配置類,開啟@Async功能支持
使用@EnableAsync來開啟異步任務支持,@EnableAsync注解可以直接放在SpringBoot啟動類上,也可以單獨放在其他配置類上。我們這里選擇使用單獨的配置類SyncConfiguration。
@Configuration //主要是為了掃描范圍包下的所有 @Async注解 @EnableAsync public class AsyncConfiguration {}3.2、在方法上標記異步調(diào)用
增加一個Component類,用來進行業(yè)務處理,同時添加@Async注解,代表該方法為異步處理;如果標注在類上,則類里面的所有方法都是需要異步處理的。
@Component @Slf4j public class AsyncTask {@SneakyThrows@Asyncpublic void doTask1() {long t1 = System.currentTimeMillis();Thread.sleep(2000);long t2 = System.currentTimeMillis();log.info("task1 cost {} ms" , t2-t1);}@SneakyThrows@Asyncpublic void doTask2() {long t1 = System.currentTimeMillis();Thread.sleep(3000);long t2 = System.currentTimeMillis();log.info("task2 cost {} ms" , t2-t1);} }3.3、在Controller中進行異步方法調(diào)用
@RestController @RequestMapping("/async") @Slf4j public class AsyncController {@Autowiredprivate AsyncTask asyncTask;@RequestMapping("/task")public void task() throws InterruptedException {long t1 = System.currentTimeMillis();asyncTask.doTask1();asyncTask.doTask2();Thread.sleep(1000);long t2 = System.currentTimeMillis();log.info("main cost {} ms", t2-t1);} }通過訪問http://localhost:8080/async/task查看控制臺日志:主線程不需要等待異步方法執(zhí)行完成,減少了響應時間,提高了接口性能。
2021-11-25 15:48:37 [http-nio-8080-exec-8] INFO AsyncController:26 - main cost 1009 ms 2021-11-25 15:48:38 [task-1] INFO ?com.async.AsyncTask:22 - task1 cost 2005 ms 2021-11-25 15:48:39 [task-2] INFO ?com.async.AsyncTask:31 - task2 cost 3005 ms通過上面三步我們就可以在SpringBoot中歡樂的使用異步方法來提高我們接口性能了,是不是很簡單?不過,如果你在實際項目開發(fā)中真這樣寫了,肯定會被老鳥們無情嘲諷?因為上面的代碼忽略了一個最大的問題,就是給@Async異步框架自定義線程池 。
四、@Async自定義線程池
4.1、為什么要給@Async自定義線程池?
使用@Async注解,在默認情況下用的是SimpleAsyncTaskExecutor線程池,該線程池不是真正意義上的線程池?。使用此線程池無法實現(xiàn)線程重用,每次調(diào)用都會新建一條線程。若系統(tǒng)中不斷的創(chuàng)建線程,最終會導致系統(tǒng)占用內(nèi)存過高,引發(fā)OutOfMemoryError錯誤。
關鍵代碼如下:
public void execute(Runnable task, long startTimeout){Assert.notNull(task, "Runnable must not be null");Runnable taskToUse = this.taskDecorator != null ? this.taskDecorator.decorate(task) : task;//判斷是否開啟限流,默認為否if (this.isThrottleActive() && startTimeout > 0L) {//執(zhí)行前置操作,進行限流this.concurrencyThrottle.beforeAccess();this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));} else {//未限流的情況,執(zhí)行線程任務this.doExecute(taskToUse);}}protected void doExecute(Runnable task) {//不斷創(chuàng)建線程Thread thread = this.threadFactory != null ? this.threadFactory.newThread(task) : this.createThread(task);thread.start(); }//創(chuàng)建線程 public Thread createThread(Runnable runnable) {//指定線程名,task-1,task-2...Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());thread.setPriority(this.getThreadPriority());thread.setDaemon(this.isDaemon());return thread; }我們也可以直接通過上面的控制臺日志觀察,每次打印的線程名都是[task-1]、[task-2]、[task-3]、[task-4].....遞增的。所以我們在使用Spring中的@Async異步框架時一定要自定義線程池,替代默認的SimpleAsyncTaskExecutor。
Spring提供了多種線程池:
-
SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調(diào)用都會創(chuàng)建一個新的線程。
-
SyncTaskExecutor:這個類沒有實現(xiàn)異步調(diào)用,只是一個同步操作。只適用于不需要多線程的地方。
-
ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類
-
ThreadPoolTaskScheduler:可以使用cron表達式
-
ThreadPoolTaskExecutor?:最常使用,推薦。其實質(zhì)是對java.util.concurrent.ThreadPoolExecutor的包裝
4.2、為@Async實現(xiàn)一個自定義線程池
@Configuration @EnableAsync public class SyncConfiguration {@Bean(name = "asyncPoolTaskExecutor")public ThreadPoolTaskExecutor executor() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();//核心線程數(shù)taskExecutor.setCorePoolSize(10);//線程池維護線程的最大數(shù)量,只有在緩沖隊列滿了之后才會申請超過核心線程數(shù)的線程taskExecutor.setMaxPoolSize(100);//緩存隊列taskExecutor.setQueueCapacity(50);//許的空閑時間,當超過了核心線程出之外的線程在空閑時間到達之后會被銷毀taskExecutor.setKeepAliveSeconds(200);//異步方法內(nèi)部線程名稱taskExecutor.setThreadNamePrefix("async-");/*** 當線程池的任務緩存隊列已滿并且線程池中的線程數(shù)目達到maximumPoolSize,如果還有任務到來就會采取任務拒絕策略* 通常有以下四種策略:* ThreadPoolExecutor.AbortPolicy:丟棄任務并拋出RejectedExecutionException異常。* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執(zhí)行任務(重復此過程)* ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重復調(diào)用 execute() 方法,直到成功*/taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());taskExecutor.initialize();return taskExecutor;} }配置自定義線程池以后我們就可以大膽的使用@Async提供的異步處理能力了。
4.3、@Async配置默認線程池和多個線程池處理
在現(xiàn)實的互聯(lián)網(wǎng)項目開發(fā)中,針對高并發(fā)的請求,一般的做法是高并發(fā)接口單獨線程池隔離處理。
假設現(xiàn)在2個高并發(fā)接口:一個是修改用戶信息接口,刷新用戶redis緩存;一個是下訂單接口,發(fā)送app push信息。往往會根據(jù)接口特征定義兩個線程池,這時候我們在使用@Async時就需要通過指定線程池名稱進行區(qū)分。
(1)為@Async指定線程池名字
@SneakyThrows @Async("asyncPoolTaskExecutor") public void doTask1() {long t1 = System.currentTimeMillis();Thread.sleep(2000);long t2 = System.currentTimeMillis();log.info("task1 cos }當系統(tǒng)存在多個線程池時,我們也可以配置一個默認線程池,對于非默認的異步任務再通過@Async("otherTaskExecutor")來指定線程池名稱。
(2)配置默認線程池
可以修改配置類讓其實現(xiàn)AsyncConfigurer,并重寫getAsyncExecutor()方法,指定默認線程池:
@Configuration @EnableAsync @Slf4j public?class?AsyncConfiguration?implements?AsyncConfigurer?{@Bean(name?=?"asyncPoolTaskExecutor")public?ThreadPoolTaskExecutor?executor()?{ThreadPoolTaskExecutor?taskExecutor?=?new?ThreadPoolTaskExecutor();//核心線程數(shù)taskExecutor.setCorePoolSize(2);//線程池維護線程的最大數(shù)量,只有在緩沖隊列滿了之后才會申請超過核心線程數(shù)的線程taskExecutor.setMaxPoolSize(10);//緩存隊列taskExecutor.setQueueCapacity(50);//許的空閑時間,當超過了核心線程出之外的線程在空閑時間到達之后會被銷毀taskExecutor.setKeepAliveSeconds(200);//異步方法內(nèi)部線程名稱taskExecutor.setThreadNamePrefix("async-");/***?當線程池的任務緩存隊列已滿并且線程池中的線程數(shù)目達到maximumPoolSize,如果還有任務到來就會采取任務拒絕策略*?通常有以下四種策略:* ThreadPoolExecutor.AbortPolicy:丟棄任務并拋出RejectedExecutionException異常。* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執(zhí)行任務(重復此過程)* ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重復調(diào)用 execute()?方法,直到成功*/taskExecutor.setRejectedExecutionHandler(new?ThreadPoolExecutor.CallerRunsPolicy());taskExecutor.initialize();return?taskExecutor;}/***?指定默認線程池*/@Overridepublic?Executor?getAsyncExecutor()?{return?executor();}@Overridepublic?AsyncUncaughtExceptionHandler?getAsyncUncaughtExceptionHandler()?{return?(ex,?method,?params)?->log.error("線程池執(zhí)行任務發(fā)送未知錯誤,執(zhí)行方法:{}",method.getName(),ex);} }如下,doTask1()方法使用默認使用線程池asyncPoolTaskExecutor,doTask2()使用線程池otherTaskExecutor,非常靈活。
@Async public void doTask1() {long t1 = System.currentTimeMillis();Thread.sleep(2000);long t2 = System.currentTimeMillis();log.info("task1 cost {} ms" , t2-t1); }@SneakyThrows @Async("otherTaskExecutor") public void doTask2() {long t1 = System.currentTimeMillis();Thread.sleep(3000);long t2 = System.currentTimeMillis();log.info("task2 cost {} ms" , t2-t1); }@Async異步方法在日常開發(fā)中經(jīng)常會用到,很有必要掌握。
參考鏈接:
Java異步調(diào)用方法
SpringBoot 如何實現(xiàn)異步編程,老鳥們都這么玩的!
@Async異步執(zhí)行
Spring使用@Async注解
Java 異步編程的幾種方式
總結
以上是生活随笔為你收集整理的SpringBoot利用@Async注解实现异步调用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 键盘操作练习
- 下一篇: Mac怎么设置自动关机?