springboot中@Async默认线程池导致OOM问题
作者:ignorewho 发布时间:2022-12-19 20:07:01
前言:
1.最近项目上在测试人员压测过程中发现了OOM问题,项目使用springboot搭建项目工程,通过查看日志中包含信息:unable to create new native thread
内存溢出的三种类型:
1.第一种OutOfMemoryError: PermGen space,发生这种问题的原意是程序中使用了大量的jar或class
2.第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多
3.第三种OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大
初步分析:
1.初步怀疑是线程创建太多导致,使用jstack 线程号 > /tmp/oom.log将应用的线程信息打印出来。查看oom.log,发现大量线程处于Runnable状态,基本可以确认是线程创建太多了。
代码分析:
1.出问题的微服务是日志写库服务,对比日志,锁定在writeLog方法上,wirteLog方法使用spring-@Async注解,写库操作采用的是异步写入方式。
2.之前没有对@Async注解深入研究过,只是知道可以自定义内部线程池,经查看,日志写库服务并未自定义异步配置,使用的是spring-@Async默认异步配置
3.首先简单百度了下,网上提到@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,在压测情况下,会有大量写库请求进入日志写库服务,这时就会不断创建大量线程,极有可能压爆服务器内存。
借此机会也学习了下SimpleAsyncTaskExecutor源码,总结如下:
1.SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务,核心代码如下:
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) {
//执行前置操作,进行限流
this.concurrencyThrottle.beforeAccess();
//执行完线程任务,会执行后置操作concurrencyThrottle.afterAccess(),配合进行限流
doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
}
else {
doExecute(taskToUse);
}
}
2.SimpleAsyncTaskExecutor限流实现
首先任务进来,会循环判断当前执行线程数是否超过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限流实现:首先任务进来,会循环判断当前执行线程数是否超过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++;
}
}
}
线程任务执行完毕后,当前执行线程数会减一,会调用monitor对象的notify方法,唤醒等待状态下的线程,等待状态下的线程会竞争monitor锁,竞争到,会继续执行线程任务。
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();
//设置允许同时执行的线程数为10
executor.setConcurrencyLimit(10);
return executor;
}
}
同样,我们启动20个线程去调用异步方法,查看Java VisualVM工具如下:
通过上面验证可知:
1.开启限流情况下,能有效控制应用线程数
2.虽然可以有效控制线程数,但执行效率会降低,会出现主线程等待,线程竞争的情况。
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("线程池执行任务发生未知异常.", ex);
}
};
}
}
来源:https://blog.csdn.net/ignorewho/article/details/85603920
猜你喜欢
- Netty是一个非常优秀的Socket框架。如果需要在SpringBoot开发的app中,提供Socket服务,那么Netty是不错的选择。
- 前言在电商的应用中,最常见的就是在首页或完成某事件之后,弹出一堆的活动/广告。假如重叠弹出,很丑,给用户的体验也不好,所以一般都会依次依条件
- Java实现远程控制技术java自带的java.net.和java.awt.robot. 的混合可以用于实现通过网络对另一台计算机的远程控制
- 在应用程序中使用多个线程的一个好处是每个线程都可以异步执行。对于 Windows 应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件
- Java连接SQL Server数据库的详细操作流程一.明确JDK版本和下载驱动1.1 JDK版本查看win + r输入cmd,命令窗口输入
- 我就废话不多说了,大家还是直接看代码吧!public static String mapToTxt(Map<String,String
- 1、这个方法在GpsNET.JT809Core里来源:https://www.cnblogs.com/cnsend/p/12197982.h
- 本文实例为大家分享了C#实现猜数字游戏具体代码,供大家参考,具体内容如下给定一个0-100的随机数字猜其大小题目样式:电脑产生一个0到100
- 本文实例为大家分享了java实现顺时针打印矩阵的具体代码,供大家参考,具体内容如下题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每
- 1.冒泡排序简介冒泡排序(Bubble Sorting)即:通过对待排序的序列从前往后,依次比较相邻元素的值,若发现逆序则交换位置,使较大的
- using System; using System.Collections.Generic; using System.Linq; usi
- 实现多线程的方式:实现多线程的方式有多种,这里只列举两种常用的,而第一种继承Thread的方式无法实现多窗口卖票。一,继承Thread方式:
- 内存泄漏:是指内存得不到GC的及时回收,从而造成内存占用过多,从而导致程序Crash,也就是常说的OOM。 一、static 先来看下面一段
- 要点有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。使用注解来映射简单语句会使代码显
- 我们知道,Object类是所有类的父类,因此也被称为根类、祖先。那么,我们就来看一看Object类的最常用的两个方法是如何用的。1.toSt
- 效果展示在实际项目当中我们经常看到如下各种剪裁形状的效果,Flutter 为我们提供了非常方便的 Widget 很轻松就可以实现,下面我们来
- 1. 出故障了没办法,干it这一行,就得天天面对故障,大家就是传说中的消防员,到处救火。不过,这次的故障范围有点大,宿主机都打不开了。好在监
- public class Change{ public static void main(String args[]){ fin
- 本文实例讲述了Java对XML文件增删改查操作。分享给大家供大家参考,具体如下:xml文件:<?xml version="1
- 本文以Java程序代码为例展示如何通过格式转换的方式将PPT幻灯片文档转为HTML文件。这里的PPT幻灯片可以是.ppt/.pptx/.pp