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

