springboot中@Async默認線程池導致OOM問題
前言:
1.最近項目上在測試人員壓測過程中發(fā)現了OOM問題,項目使用springboot搭建項目工程,通過查看日志中包含信息:unable to create new native thread
內存溢出的三種類型:
1.第一種OutOfMemoryError: PermGen space,發(fā)生這種問題的原意是程序中使用了大量的jar或class
2.第二種OutOfMemoryError: Java heap space,發(fā)生這種問題的原因是java虛擬機創(chuàng)建的對象太多
3.第三種OutOfMemoryError:unable to create new native thread,創(chuàng)建線程數量太多,占用內存過大
初步分析:
1.初步懷疑是線程創(chuàng)建太多導致,使用jstack 線程號 > /tmp/oom.log將應用的線程信息打印出來。查看oom.log,發(fā)現大量線程處于Runnable狀態(tài),基本可以確認是線程創(chuàng)建太多了。
代碼分析:
1.出問題的微服務是日志寫庫服務,對比日志,鎖定在writeLog方法上,wirteLog方法使用spring-@Async注解,寫庫操作采用的是異步寫入方式。
2.之前沒有對@Async注解深入研究過,只是知道可以自定義內部線程池,經查看,日志寫庫服務并未自定義異步配置,使用的是spring-@Async默認異步配置
3.首先簡單百度了下,網上提到@Async默認異步配置使用的是SimpleAsyncTaskExecutor,該線程池默認來一個任務創(chuàng)建一個線程,在壓測情況下,會有大量寫庫請求進入日志寫庫服務,這時就會不斷創(chuàng)建大量線程,極有可能壓爆服務器內存。
借此機會也學習了下SimpleAsyncTaskExecutor源碼,總結如下:
1.SimpleAsyncTaskExecutor提供了限流機制,通過concurrencyLimit屬性來控制開關,當concurrencyLimit>=0時開啟限流機制,默認關閉限流機制即concurrencyLimit=-1,當關閉情況下,會不斷創(chuàng)建新的線程來處理任務,核心代碼如下:
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 (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) { //執(zhí)行前置操作,進行限流 this.concurrencyThrottle.beforeAccess(); //執(zhí)行完線程任務,會執(zhí)行后置操作concurrencyThrottle.afterAccess(),配合進行限流 doExecute(new ConcurrencyThrottlingRunnable(taskToUse)); } else { doExecute(taskToUse); } }
2.SimpleAsyncTaskExecutor限流實現
首先任務進來,會循環(huán)判斷當前執(zhí)行線程數是否超過concurrencyLimit,如果超了,則當前線程調用wait方法,釋放monitor對象鎖,進入等待
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限流實現:首先任務進來,會循環(huán)判斷當前執(zhí)行線程數是否超過concurrencyLimit,如果超了,則當前線程調用wait方法,釋放monitor對象鎖,進入等待狀態(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++; } } }
線程任務執(zhí)行完畢后,當前執(zhí)行線程數會減一,會調用monitor對象的notify方法,喚醒等待狀態(tài)下的線程,等待狀態(tài)下的線程會競爭monitor鎖,競爭到,會繼續(xù)執(zhí)行線程任務。
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有限流機制,實踐出真知,我們還是測試下:
一、測試未開啟限流機制下,我們啟動20個線程去調用異步方法,查看Java VisualVM工具如下:
二、測試開啟限流機制,開啟限流機制的代碼如下:
@Configuration @EnableAsync public class AsyncCommonConfig extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); //設置允許同時執(zhí)行的線程數為10 executor.setConcurrencyLimit(10); return executor; } }
同樣,我們啟動20個線程去調用異步方法,查看Java VisualVM工具如下:
通過上面驗證可知:
1.開啟限流情況下,能有效控制應用線程數
2.雖然可以有效控制線程數,但執(zhí)行效率會降低,會出現主線程等待,線程競爭的情況。
3.限流機制適用于任務處理比較快的場景,對于應用處理時間比較慢的場景并不適用。==
最終解決辦法:
1.自定義線程池,使用LinkedBlockingQueue阻塞隊列來限定線程池的上限
2.定義拒絕策略,如果隊列滿了,則拒絕處理該任務,打印日志,代碼如下:
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("當前任務線程池隊列已滿."); }); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncUncaughtExceptionHandler() { @Override public void handleUncaughtException(Throwable ex , Method method , Object... params) { logger.error("線程池執(zhí)行任務發(fā)生未知異常.", ex); } }; } }
到此這篇關于springboot中@Async默認線程池導致OOM問題的文章就介紹到這了,更多相關springboot @Async線程池導致OOM內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java讀取配置文件(properties)的時候,unicode碼轉utf-8方式
這篇文章主要介紹了java讀取配置文件(properties)的時候,unicode碼轉utf-8方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02SpringBoot整合Mybatis與thymleft實現增刪改查功能詳解
MybatisPlus是國產的第三方插件,?它封裝了許多常用的CURDapi,免去了我們寫mapper.xml的重復勞動。本文將整合MybatisPlus實現增刪改查功能,感興趣的可以了解一下2022-12-12Mybatis?Plus插入數據后獲取新數據id值的踩坑記錄
在某些情況下,需要在執(zhí)行新增后,需要獲取到新增行的id,這篇文章主要給大家介紹了關于Mybatis?Plus插入數據后獲取新數據id值的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08詳解spring cloud hystrix 請求合并collapsing
這篇文章主要介紹了詳解spring cloud hystrix 請求合并collapsing,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05