深度解析Spring Boot @Async 正在榨干服務(wù)器資源
“服務(wù)又掛了?日志里全是 OutOfMemoryError: unable to create new native thread!”
你是否也曾深夜被這樣的報(bào)警驚醒?明明只是一個(gè)簡單的異步處理,用 @Async 注解輕輕一標(biāo),本地測試跑得飛快,一到高并發(fā)壓測就瞬間“猝死”。這幾乎是每個(gè)從初級走向中級的 Java 開發(fā)者都會(huì)踩的坑。
問題出在哪里?出在你以為 @Async 是“銀彈”,但實(shí)際上,你只是把它當(dāng)成了一把沒加防護(hù)的“電鋸”在用。今天,我們就來徹底搞懂 @Async 背后的線程池,并學(xué)會(huì)如何像個(gè)老手一樣駕馭它,讓你的應(yīng)用在高并發(fā)下穩(wěn)如泰山。
核心概念拆解:把線程池當(dāng)成一個(gè)“智慧廚房”
忘掉那些枯燥的 API 定義,我們用一個(gè)更形象的方式來理解線程池的核心思想。
想象一下,你開了一家生意火爆的餐廳。
- 請求 (Request):源源不斷進(jìn)店點(diǎn)餐的顧客。
- 線程 (Thread):負(fù)責(zé)炒菜的廚師。
方案一:裸奔模式 (不使用線程池) 每來一位顧客,你就臨時(shí)去人才市場招一位新廚師。顧客少的時(shí)候還行,一旦到了飯點(diǎn)高峰期,幾百個(gè)顧客同時(shí)涌入,你的小廚房瞬間塞滿了臨時(shí)廚師,亂作一團(tuán),最終廚房(服務(wù)器內(nèi)存)被擠爆,餐廳(服務(wù))直接關(guān)門大吉。這就是 unable to create new native thread 的本質(zhì)——系統(tǒng)資源被無限創(chuàng)建的線程耗盡了。
方案二:智慧廚房模式 (使用線程池) 你學(xué)聰明了,建立了一個(gè)專業(yè)的廚師團(tuán)隊(duì)(線程池)。
- 核心廚師 (corePoolSize):你有 5 位正式廚師,他們是廚房的中堅(jiān)力量,時(shí)刻準(zhǔn)備接單。
- 等餐區(qū) (workQueue):當(dāng) 5 位廚師都在忙時(shí),新來的訂單會(huì)先放到一個(gè)等餐區(qū)(任務(wù)隊(duì)列)排隊(duì)。
- 臨時(shí)工 (maximumPoolSize):如果等餐區(qū)的訂單也排滿了(比如超過 100 單),說明生意實(shí)在太火爆了。你決定臨時(shí)再請 3 位廚師來幫忙,這樣廚房最多就有 8 位廚師。
- 拒絕策略 (RejectedExecutionHandler):如果 8 位廚師全在忙,等餐區(qū)也滿了,再有新顧客來,服務(wù)員(拒絕策略)就會(huì)禮貌地告訴他:“抱歉,今天太忙了,您稍后再來吧。” 而不是讓顧客無止境地等下去,最終導(dǎo)致體驗(yàn)崩潰。
- 臨時(shí)工的補(bǔ)貼 (keepAliveTime):高峰期過后,那 3 位臨時(shí)工如果連續(xù) 1 分鐘都沒接到新活,你就會(huì)讓他們下班,以節(jié)省成本(釋放空閑線程資源),只保留 5 位核心廚師。
看到了嗎?一個(gè)設(shè)計(jì)良好的線程池,就像一個(gè)管理有序的智慧廚房,它通過復(fù)用、排隊(duì)和限流,確保了在任何客流量下,廚房都能高效、穩(wěn)定地運(yùn)轉(zhuǎn),而不是無限擴(kuò)張導(dǎo)致崩潰。
實(shí)戰(zhàn)落地:在 Spring Boot 中正確“馴服”@Async
光說不練假把式。我們來看如何在 Spring Boot 項(xiàng)目中,從“裸奔”模式進(jìn)化到“精細(xì)化”管理模式。
1. 依賴(Maven)
你只需要 Spring Boot 的 Web 啟動(dòng)器,@Async 的支持是內(nèi)置的。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version> <!-- 請根據(jù)你的項(xiàng)目選擇合適的版本 -->
</dependency>2. 啟用異步功能
在你的主啟動(dòng)類上添加 @EnableAsync 注解。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync // 開啟異步任務(wù)支持
public class HighConcurrencyApplication {
public static void main(String[] args) {
SpringApplication.run(HighConcurrencyApplication.class, args);
}
}3. 錯(cuò)誤的用法(裸奔的@Async)
很多新手會(huì)直接在一個(gè) Service 方法上標(biāo)注 @Async,就像這樣:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Async // 警告:這是默認(rèn)配置,高并發(fā)下極度危險(xiǎn)!
public void processOrder(String orderId) {
// 模擬處理訂單的耗時(shí)操作
System.out.println(Thread.currentThread().getName() + " 開始處理訂單: " + orderId);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 訂單處理完成: " + orderId);
}
}為什么危險(xiǎn)? 因?yàn)樵跊]有自定義線程池的情況下,Spring Boot 會(huì)使用一個(gè) SimpleAsyncTaskExecutor。這個(gè)執(zhí)行器不會(huì)復(fù)用線程,每次調(diào)用都會(huì)創(chuàng)建一個(gè)新線程。這和我們“智慧廚房”的反面教材——方案一,一模一樣。
4. 正確的姿勢:自定義線程池
我們需要?jiǎng)?chuàng)建一個(gè)配置類,來定義我們自己的“智慧廚房”。
ThreadPoolConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadPoolConfig {
// Bean 的名稱,我們將在 @Async 注解中引用它
public static final String ASYNC_EXECUTOR_NAME = "myAsyncExecutor";
@Bean(name = ASYNC_EXECUTOR_NAME)
public Executor myAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心廚師數(shù)量:根據(jù)CPU核心數(shù)動(dòng)態(tài)計(jì)算
int corePoolSize = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(corePoolSize);
// 最大廚師數(shù)量:通常是核心數(shù)的2-3倍
executor.setMaxPoolSize(corePoolSize * 2);
// 等餐區(qū)容量
executor.setQueueCapacity(100);
// 臨時(shí)工的存活時(shí)間
executor.setKeepAliveSeconds(60);
// 給廚師起個(gè)好名字,方便排查問題
executor.setThreadNamePrefix("My-Async-");
// 拒絕策略:讓調(diào)用者自己處理,這是最穩(wěn)妥的方式
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}application.yml (推薦)
為了靈活性,最好將核心參數(shù)配置在 application.yml 中。
myapp:
async:
core-pool-size: 8
max-pool-size: 16
queue-capacity: 200
keep-alive-seconds: 60然后在配置類中通過 @Value 注入這些值。
5. 在業(yè)務(wù)代碼中指定線程池
現(xiàn)在,讓我們的 OrderService 使用新定義的“智慧廚房”。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.yourpackage.config.ThreadPoolConfig; // 引入你的配置類
@Service
public class OrderService {
// 通過 value 屬性指定要使用的線程池 Bean 名稱
@Async(ThreadPoolConfig.ASYNC_EXECUTOR_NAME)
public void processOrder(String orderId) {
// ... 業(yè)務(wù)邏輯不變 ...
System.out.println("使用自定義線程池 [" + Thread.currentThread().getName() + "] 開始處理訂單: " + orderId);
// ...
}
}現(xiàn)在,所有的異步訂單處理任務(wù)都會(huì)被提交到我們精心設(shè)計(jì)的 myAsyncExecutor 線程池中,再也不怕野線程滿天飛了。
避坑指南:生產(chǎn)環(huán)境的“秘密”
- 小心“無底洞”隊(duì)列:千萬不要將
queueCapacity設(shè)置得過大,或者使用Executors.newFixedThreadPool(),它內(nèi)部使用的是無界的LinkedBlockingQueue。在高并發(fā)下,如果任務(wù)處理速度跟不上生產(chǎn)速度,會(huì)導(dǎo)致請求在隊(duì)列中大量堆積,最終引發(fā) OOM。有限的隊(duì)列是保護(hù)系統(tǒng)的第一道防線。 - 優(yōu)雅停機(jī)很重要:Spring Boot 默認(rèn)會(huì)等待異步任務(wù)執(zhí)行完畢再關(guān)閉應(yīng)用。但你需要通過
spring.task.execution.shutdown.await-termination=true和spring.task.execution.shutdown.await-termination-period來確保有足夠的時(shí)間讓任務(wù)完成,防止服務(wù)關(guān)閉時(shí)丟失正在處理的數(shù)據(jù)。 - 別忘了線程上下文:如果你在異步方法中需要獲取
RequestContext(如用戶信息、TraceID),默認(rèn)情況下是拿不到的。你需要自己處理上下文的傳遞,或者使用像TransmittableThreadLocal這樣的庫來自動(dòng)完成。這又是另一個(gè)深坑,值得單獨(dú)寫一篇文章。
總結(jié)與升華
我們今天從一個(gè)常見的 OOM 慘案出發(fā),通過一個(gè)“智慧廚房”的比喻,徹底理解了線程池的工作原理。核心的收獲是:
永遠(yuǎn)不要在生產(chǎn)環(huán)境中使用默認(rèn)的
@Async,必須為其提供一個(gè)經(jīng)過精細(xì)化配置的自定義線程池。
這不僅是代碼技巧,更是一種對系統(tǒng)負(fù)責(zé)的架構(gòu)思維。我們從“能用”走向了“可靠、可控”。
最后,留一個(gè)思考題:我們今天討論的都是單個(gè)應(yīng)用內(nèi)的線程池。如果我們的系統(tǒng)是分布式的微服務(wù)架構(gòu),一個(gè)請求需要跨越多個(gè)服務(wù),我們又該如何控制整個(gè)鏈路的并發(fā)和資源,防止上游的流量洪峰打垮下游所有服務(wù)呢?
到此這篇關(guān)于Spring Boot @Async 正在榨干服務(wù)器資源的文章就介紹到這了,更多相關(guān)Spring Boot @Async服務(wù)器資源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot @Async如何自定義線程池及使用教程
- Springboot?配置線程池創(chuàng)建線程及配置?@Async?異步操作線程池詳解
- Spring Boot之@Async異步線程池示例詳解
- SpringBoot異步使用@Async的原理以及線程池配置詳解
- Springboot 如何使用@Async整合線程池
- springboot中@Async默認(rèn)線程池導(dǎo)致OOM問題
- Spring boot注解@Async線程池實(shí)例詳解
- Spring Boot利用@Async異步調(diào)用:ThreadPoolTaskScheduler線程池的優(yōu)雅關(guān)閉詳解
- Spring Boot利用@Async如何實(shí)現(xiàn)異步調(diào)用:自定義線程池
相關(guān)文章
Springboot如何優(yōu)雅的關(guān)閉應(yīng)用
這篇文章主要介紹了Springboot如何優(yōu)雅的關(guān)閉應(yīng)用問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
詳解Springboot快速搭建跨域API接口的步驟(idea社區(qū)版2023.1.4+apache-maven-3.9.
這篇文章主要介紹了Springboot快速搭建跨域API接口(idea社區(qū)版2023.1.4+apache-maven-3.9.3-bin),本文通過圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
@RunWith(SpringJUnit4ClassRunner.class)報(bào)錯(cuò)問題及解決
這篇文章主要介紹了@RunWith(SpringJUnit4ClassRunner.class)報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
springboot如何通過controller層實(shí)現(xiàn)頁面切換
在Spring Boot中,通過Controller層實(shí)現(xiàn)頁面切換背景,Spring Boot的默認(rèn)注解是@RestController,它包含了@Controller和@ResponseBody,@ResponseBody會(huì)將返回值轉(zhuǎn)換為字符串返回,因此無法實(shí)現(xiàn)頁面切換,將@RestController換成@Controller2024-12-12
新建一個(gè)springboot單體項(xiàng)目的教程
這篇文章主要介紹了新建一個(gè)springboot單體項(xiàng)目的教程,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由
這篇文章主要介紹了SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
Java實(shí)現(xiàn)用位運(yùn)算維護(hù)狀態(tài)碼
位運(yùn)算是一種非常高效的運(yùn)算方式,在算法考察中比較常見,那么業(yè)務(wù)代碼中我們?nèi)绾问褂梦贿\(yùn)算呢,感興趣的小伙伴快跟隨小編一起學(xué)習(xí)一下吧2024-03-03
Java實(shí)現(xiàn)日志文件監(jiān)聽并讀取相關(guān)數(shù)據(jù)的方法實(shí)踐
本文主要介紹了Java實(shí)現(xiàn)日志文件監(jiān)聽并讀取相關(guān)數(shù)據(jù)的方法實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

