SpringBoot 如何實(shí)現(xiàn)異步編程
首先我們來看看在Spring中為什么要使用異步編程,它能解決什么問題?
為什么要用異步框架,它解決什么問題?
在SpringBoot的日常開發(fā)中,一般都是同步調(diào)用的。但實(shí)際中有很多場景非常適合使用異步來處理,如:注冊新用戶,送100個積分;或下單成功,發(fā)送push消息等等。
就拿注冊新用戶這個用例來說,為什么要異步處理?
- 第一個原因:容錯性、健壯性,如果送積分出現(xiàn)異常,不能因?yàn)樗头e分而導(dǎo)致用戶注冊失??;
- 因?yàn)橛脩糇允侵饕δ?,送積分是次要功能,即使送積分異常也要提示用戶注冊成功,然后后面在針對積分異常做補(bǔ)償處理。
- 第二個原因:提升性能,例如注冊用戶花了20毫秒,送積分花費(fèi)50毫秒,如果用同步的話,總耗時70毫秒,用異步的話,無需等待積分,故耗時20毫秒。
故,異步能解決2個問題,性能和容錯性。
SpringBoot如何實(shí)現(xiàn)異步調(diào)用?
對于異步方法調(diào)用,從Spring3開始提供了@Async注解,我們只需要在方法上標(biāo)注此注解,此方法即可實(shí)現(xiàn)異步調(diào)用。
當(dāng)然,我們還需要一個配置類,通過Enable模塊驅(qū)動注解@EnableAsync 來開啟異步功能。
實(shí)現(xiàn)異步調(diào)用
第一步:新建配置類,開啟@Async功能支持
使用@EnableAsync來開啟異步任務(wù)支持,@EnableAsync注解可以直接放在SpringBoot啟動類上,也可以單獨(dú)放在其他配置類上。我們這里選擇使用單獨(dú)的配置類SyncConfiguration。
@Configuration
@EnableAsync
public class AsyncConfiguration {
}
第二步:在方法上標(biāo)記異步調(diào)用
增加一個Component類,用來進(jìn)行業(yè)務(wù)處理,同時添加@Async注解,代表該方法為異步處理。
@Component
@Slf4j
public class AsyncTask {
@SneakyThrows
@Async
public void doTask1() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms" , t2-t1);
}
@SneakyThrows
@Async
public void doTask2() {
long t1 = System.currentTimeMillis();
Thread.sleep(3000);
long t2 = System.currentTimeMillis();
log.info("task2 cost {} ms" , t2-t1);
}
}
第三步:在Controller中進(jìn)行異步方法調(diào)用
@RestController
@RequestMapping("/async")
@Slf4j
public class AsyncController {
@Autowired
private 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查看控制臺日志:
2021-11-25 15:48:37 [http-nio-8080-exec-8] INFO? com.jianzh5.blog.async.AsyncController:26 - main cost 1009 ms
2021-11-25 15:48:38 [task-1] INFO? com.jianzh5.blog.async.AsyncTask:22 - task1 cost 2005 ms
2021-11-25 15:48:39 [task-2] INFO? com.jianzh5.blog.async.AsyncTask:31 - task2 cost 3005 ms
通過日志可以看到:主線程不需要等待異步方法執(zhí)行完成,減少響應(yīng)時間,提高接口性能。
通過上面三步我們就可以在SpringBoot中歡樂的使用異步方法來提高我們接口性能了,是不是很簡單?
不過,如果真實(shí)項(xiàng)目中你真這樣寫了,肯定會被老鳥們無情嘲諷,就這?
因?yàn)樯厦娴拇a忽略了一個最大的問題,就是給@Async異步框架自定義線程池。
為什么要給@Async自定義線程池?
使用@Async注解,在默認(rèn)情況下用的是SimpleAsyncTaskExecutor線程池,該線程池不是真正意義上的線程池。
使用此線程池?zé)o法實(shí)現(xiàn)線程重用,每次調(diào)用都會新建一條線程。若系統(tǒng)中不斷的創(chuàng)建線程,最終會導(dǎo)致系統(tǒng)占用內(nèi)存過高,引發(fā)OutOfMemoryError錯誤,關(guān)鍵代碼如下:
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;
//判斷是否開啟限流,默認(rèn)為否
if (this.isThrottleActive() && startTimeout > 0L) {
//執(zhí)行前置操作,進(jìn)行限流
this.concurrencyThrottle.beforeAccess();
this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
} else {
//未限流的情況,執(zhí)行線程任務(wù)
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異步框架時一定要自定義線程池,替代默認(rèn)的SimpleAsyncTaskExecutor。
Spring提供了多種線程池:
- SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調(diào)用都會創(chuàng)建一個新的線程。
- SyncTaskExecutor:這個類沒有實(shí)現(xiàn)異步調(diào)用,只是一個同步操作。只適用于不需要多線程的地
- ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類
- ThreadPoolTaskScheduler:可以使用cron表達(dá)式
- ThreadPoolTaskExecutor :最常使用,推薦。 其實(shí)質(zhì)是對java.util.concurrent.ThreadPoolExecutor的包裝
為@Async實(shí)現(xiàn)一個自定義線程池
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心線程數(shù)
taskExecutor.setCorePoolSize(10);
//線程池維護(hù)線程的最大數(shù)量,只有在緩沖隊(duì)列滿了之后才會申請超過核心線程數(shù)的線程
taskExecutor.setMaxPoolSize(100);
//緩存隊(duì)列
taskExecutor.setQueueCapacity(50);
//許的空閑時間,當(dāng)超過了核心線程出之外的線程在空閑時間到達(dá)之后會被銷毀
taskExecutor.setKeepAliveSeconds(200);
//異步方法內(nèi)部線程名稱
taskExecutor.setThreadNamePrefix("async-");
/**
* 當(dāng)線程池的任務(wù)緩存隊(duì)列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當(dāng)前的任務(wù),自動重復(fù)調(diào)用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
自定義線程池以后我們就可以大膽的使用@Async提供的異步處理能力了。
多個線程池處理
在現(xiàn)實(shí)的互聯(lián)網(wǎng)項(xiàng)目開發(fā)中,針對高并發(fā)的請求,一般的做法是高并發(fā)接口單獨(dú)線程池隔離處理。
假設(shè)現(xiàn)在2個高并發(fā)接口: 一個是修改用戶信息接口,刷新用戶redis緩存; 一個是下訂單接口,發(fā)送app push信息。往往會根據(jù)接口特征定義兩個線程池,這時候我們在使用@Async時就需要通過指定線程池名稱進(jìn)行區(qū)分。
為@Async指定線程池名字
@SneakyThrows
@Async("asyncPoolTaskExecutor")
public void doTask1() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms" , t2-t1);
}
當(dāng)系統(tǒng)存在多個線程池時,我們也可以配置一個默認(rèn)線程池,對于非默認(rèn)的異步任務(wù)再通過@Async("otherTaskExecutor")來指定線程池名稱。
配置默認(rèn)線程池
可以修改配置類讓其實(shí)現(xiàn)AsyncConfigurer,并重寫getAsyncExecutor()方法,指定默認(rèn)線程池:
@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心線程數(shù)
taskExecutor.setCorePoolSize(2);
//線程池維護(hù)線程的最大數(shù)量,只有在緩沖隊(duì)列滿了之后才會申請超過核心線程數(shù)的線程
taskExecutor.setMaxPoolSize(10);
//緩存隊(duì)列
taskExecutor.setQueueCapacity(50);
//許的空閑時間,當(dāng)超過了核心線程出之外的線程在空閑時間到達(dá)之后會被銷毀
taskExecutor.setKeepAliveSeconds(200);
//異步方法內(nèi)部線程名稱
taskExecutor.setThreadNamePrefix("async-");
/**
* 當(dāng)線程池的任務(wù)緩存隊(duì)列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當(dāng)前的任務(wù),自動重復(fù)調(diào)用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
/**
* 指定默認(rèn)線程池
*/
@Override
public Executor getAsyncExecutor() {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("線程池執(zhí)行任務(wù)發(fā)送未知錯誤,執(zhí)行方法:{}",method.getName(),ex);
}
}
如下,doTask1()方法使用默認(rèn)使用線程池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);
}
小結(jié)
@Async異步方法在日常開發(fā)中經(jīng)常會用到,大家好好掌握,爭取早日成為老鳥?。?!
到此這篇關(guān)于SpringBoot 如何實(shí)現(xiàn)異步編程 的文章就介紹到這了,更多相關(guān)SpringBoot? 異步編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)MD5加密的方式與實(shí)例代碼
MD5加密是一種常見的加密方式,我們經(jīng)常用在保存用戶密碼和關(guān)鍵信息上。那么它到底有什么,又什么好處呢,會被這么廣泛的運(yùn)用在應(yīng)用開發(fā)中2021-10-10
關(guān)于java.util.Random的實(shí)現(xiàn)原理詳解
Java實(shí)用工具類庫中的類java.util.Random提供了產(chǎn)生各種類型隨機(jī)數(shù)的方法,下面這篇文章主要給大家介紹了關(guān)于java.util.Random實(shí)現(xiàn)原理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-08-08
SpringBoot Shiro授權(quán)實(shí)現(xiàn)過程解析
這篇文章主要介紹了SpringBoot Shiro授權(quán)實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
Java8實(shí)現(xiàn)任意參數(shù)的鏈棧
這篇文章主要為大家詳細(xì)介紹了Java8實(shí)現(xiàn)任意參數(shù)的鏈棧,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-10-10
JAVA實(shí)現(xiàn)空間索引編碼——GeoHash的示例
本篇文章主要介紹了JAVA實(shí)現(xiàn)空間索引編碼——GeoHash的示例,如何從眾多的位置信息中查找到離自己最近的位置,有興趣的朋友可以了解一下2016-10-10
javaweb如何實(shí)現(xiàn)請求和響應(yīng)
這篇文章主要為大家詳細(xì)介紹了javaweb如何實(shí)現(xiàn)請求和響應(yīng),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10
我用java實(shí)現(xiàn)了王者榮耀的皮膚和英雄技能
上篇文章主要實(shí)現(xiàn)了創(chuàng)建英雄,創(chuàng)建野怪,創(chuàng)建裝備.并且實(shí)現(xiàn)了簡單的刷怪,購買裝備等.本篇文章我優(yōu)化了我的操作界面,并且實(shí)現(xiàn)了英雄技能,英雄皮膚等,需要的朋友可以參考下2021-05-05

