springboot中@Async默認(rèn)線程池導(dǎo)致OOM問題
前言:
1.最近項(xiàng)目上在測試人員壓測過程中發(fā)現(xiàn)了OOM問題,項(xiàng)目使用springboot搭建項(xiàng)目工程,通過查看日志中包含信息:unable to create new native thread
內(nèi)存溢出的三種類型:
1.第一種OutOfMemoryError: PermGen space,發(fā)生這種問題的原意是程序中使用了大量的jar或class
2.第二種OutOfMemoryError: Java heap space,發(fā)生這種問題的原因是java虛擬機(jī)創(chuàng)建的對象太多
3.第三種OutOfMemoryError:unable to create new native thread,創(chuàng)建線程數(shù)量太多,占用內(nèi)存過大
初步分析:
1.初步懷疑是線程創(chuàng)建太多導(dǎo)致,使用jstack 線程號 > /tmp/oom.log將應(yīng)用的線程信息打印出來。查看oom.log,發(fā)現(xiàn)大量線程處于Runnable狀態(tài),基本可以確認(rèn)是線程創(chuàng)建太多了。
代碼分析:
1.出問題的微服務(wù)是日志寫庫服務(wù),對比日志,鎖定在writeLog方法上,wirteLog方法使用spring-@Async注解,寫庫操作采用的是異步寫入方式。
2.之前沒有對@Async注解深入研究過,只是知道可以自定義內(nèi)部線程池,經(jīng)查看,日志寫庫服務(wù)并未自定義異步配置,使用的是spring-@Async默認(rèn)異步配置
3.首先簡單百度了下,網(wǎng)上提到@Async默認(rèn)異步配置使用的是SimpleAsyncTaskExecutor,該線程池默認(rèn)來一個任務(wù)創(chuàng)建一個線程,在壓測情況下,會有大量寫庫請求進(jìn)入日志寫庫服務(wù),這時就會不斷創(chuàng)建大量線程,極有可能壓爆服務(wù)器內(nèi)存。
借此機(jī)會也學(xué)習(xí)了下SimpleAsyncTaskExecutor源碼,總結(jié)如下:
1.SimpleAsyncTaskExecutor提供了限流機(jī)制,通過concurrencyLimit屬性來控制開關(guān),當(dāng)concurrencyLimit>=0時開啟限流機(jī)制,默認(rèn)關(guān)閉限流機(jī)制即concurrencyLimit=-1,當(dāng)關(guān)閉情況下,會不斷創(chuàng)建新的線程來處理任務(wù),核心代碼如下:
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);
//判斷是否開啟限流機(jī)制
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
//執(zhí)行前置操作,進(jìn)行限流
this.concurrencyThrottle.beforeAccess();
//執(zhí)行完線程任務(wù),會執(zhí)行后置操作concurrencyThrottle.afterAccess(),配合進(jìn)行限流
doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
}
else {
doExecute(taskToUse);
}
}
2.SimpleAsyncTaskExecutor限流實(shí)現(xiàn)
首先任務(wù)進(jìn)來,會循環(huán)判斷當(dāng)前執(zhí)行線程數(shù)是否超過concurrencyLimit,如果超了,則當(dāng)前線程調(diào)用wait方法,釋放monitor對象鎖,進(jìn)入等待
protected void beforeAccess() {
if (this.concurrencyLimit == NO_CONCURRENCY) {
throw new IllegalStateException(
"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
}
if (this.concurrencyLimit > 0) {
boolean debug = logger.isDebugEnabled();
synchronized (this.monitor) {
boolean interrupted = false;
while (this.concurrencyCount >= this.concurrencyLimit) {
if (interrupted) {
throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
"but concurrency limit still does not allow for entering");
}
if (debug) {
logger.debug("Concurrency count " + this.concurrencyCount +
" has reached limit " + this.concurrencyLimit + " - blocking");
}
try {
this.monitor.wait();
}
catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other threads to react.
Thread.currentThread().interrupt();
interrupted = true;
}
}
if (debug) {
logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
}
this.concurrencyCount++;
}
}
}
2.SimpleAsyncTaskExecutor限流實(shí)現(xiàn):首先任務(wù)進(jìn)來,會循環(huán)判斷當(dāng)前執(zhí)行線程數(shù)是否超過concurrencyLimit,如果超了,則當(dāng)前線程調(diào)用wait方法,釋放monitor對象鎖,進(jìn)入等待狀態(tài)。
protected void beforeAccess() {
if (this.concurrencyLimit == NO_CONCURRENCY) {
throw new IllegalStateException(
"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
}
if (this.concurrencyLimit > 0) {
boolean debug = logger.isDebugEnabled();
synchronized (this.monitor) {
boolean interrupted = false;
while (this.concurrencyCount >= this.concurrencyLimit) {
if (interrupted) {
throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
"but concurrency limit still does not allow for entering");
}
if (debug) {
logger.debug("Concurrency count " + this.concurrencyCount +
" has reached limit " + this.concurrencyLimit + " - blocking");
}
try {
this.monitor.wait();
}
catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other threads to react.
Thread.currentThread().interrupt();
interrupted = true;
}
}
if (debug) {
logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
}
this.concurrencyCount++;
}
}
}
線程任務(wù)執(zhí)行完畢后,當(dāng)前執(zhí)行線程數(shù)會減一,會調(diào)用monitor對象的notify方法,喚醒等待狀態(tài)下的線程,等待狀態(tài)下的線程會競爭monitor鎖,競爭到,會繼續(xù)執(zhí)行線程任務(wù)。
protected void afterAccess() {
if (this.concurrencyLimit >= 0) {
synchronized (this.monitor) {
this.concurrencyCount--;
if (logger.isDebugEnabled()) {
logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount);
}
this.monitor.notify();
}
}
}
雖然看了源碼了解了SimpleAsyncTaskExecutor有限流機(jī)制,實(shí)踐出真知,我們還是測試下:
一、測試未開啟限流機(jī)制下,我們啟動20個線程去調(diào)用異步方法,查看Java VisualVM工具如下:

二、測試開啟限流機(jī)制,開啟限流機(jī)制的代碼如下:
@Configuration
@EnableAsync
public class AsyncCommonConfig extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
//設(shè)置允許同時執(zhí)行的線程數(shù)為10
executor.setConcurrencyLimit(10);
return executor;
}
}
同樣,我們啟動20個線程去調(diào)用異步方法,查看Java VisualVM工具如下:

通過上面驗(yàn)證可知:
1.開啟限流情況下,能有效控制應(yīng)用線程數(shù)
2.雖然可以有效控制線程數(shù),但執(zhí)行效率會降低,會出現(xiàn)主線程等待,線程競爭的情況。
3.限流機(jī)制適用于任務(wù)處理比較快的場景,對于應(yīng)用處理時間比較慢的場景并不適用。==
最終解決辦法:
1.自定義線程池,使用LinkedBlockingQueue阻塞隊(duì)列來限定線程池的上限
2.定義拒絕策略,如果隊(duì)列滿了,則拒絕處理該任務(wù),打印日志,代碼如下:
public class AsyncConfig implements AsyncConfigurer{
private Logger logger = LogManager.getLogger();
@Value("${thread.pool.corePoolSize:10}")
private int corePoolSize;
@Value("${thread.pool.maxPoolSize:20}")
private int maxPoolSize;
@Value("${thread.pool.keepAliveSeconds:4}")
private int keepAliveSeconds;
@Value("${thread.pool.queueCapacity:512}")
private int queueCapacity;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setQueueCapacity(queueCapacity);
executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor exe) -> {
logger.warn("當(dāng)前任務(wù)線程池隊(duì)列已滿.");
});
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable ex , Method method , Object... params) {
logger.error("線程池執(zhí)行任務(wù)發(fā)生未知異常.", ex);
}
};
}
}
到此這篇關(guān)于springboot中@Async默認(rèn)線程池導(dǎo)致OOM問題的文章就介紹到這了,更多相關(guān)springboot @Async線程池導(dǎo)致OOM內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot2線程池定義使用方法解析
- springboot2.0以上調(diào)度器配置線程池的實(shí)現(xiàn)
- Spring boot注解@Async線程池實(shí)例詳解
- Spring Boot中配置定時任務(wù)、線程池與多線程池執(zhí)行的方法
- SpringBoot策略模式的實(shí)踐使用
- 淺談Spring Boot: 接口壓測及簡要優(yōu)化策略
- 詳解SpringBoot結(jié)合策略模式實(shí)戰(zhàn)套路
- 如何修改覆蓋spring boot默認(rèn)日志策略logback詳解
- Spring Boot配置線程池拒絕策略的場景分析(妥善處理好溢出的任務(wù))
相關(guān)文章
java?io文件操作從文件讀取數(shù)據(jù)的六種方法
這篇文章主要為大家介紹了java?io操作總結(jié)從文件讀取數(shù)據(jù)的六種方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-03-03
java讀取配置文件(properties)的時候,unicode碼轉(zhuǎn)utf-8方式
這篇文章主要介紹了java讀取配置文件(properties)的時候,unicode碼轉(zhuǎn)utf-8方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot整合Mybatis與thymleft實(shí)現(xiàn)增刪改查功能詳解
MybatisPlus是國產(chǎn)的第三方插件,?它封裝了許多常用的CURDapi,免去了我們寫mapper.xml的重復(fù)勞動。本文將整合MybatisPlus實(shí)現(xiàn)增刪改查功能,感興趣的可以了解一下2022-12-12
Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的踩坑記錄
在某些情況下,需要在執(zhí)行新增后,需要獲取到新增行的id,這篇文章主要給大家介紹了關(guān)于Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
詳解spring cloud hystrix 請求合并collapsing
這篇文章主要介紹了詳解spring cloud hystrix 請求合并collapsing,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05

