问题现象
压测持续了一段时间,很多接口响应逐渐超时,无法提供服务,于是停止压测,重启机器,开始排查问题
- 观察线上日志,发现有大量线程池拒绝任务的抛出的异常
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@662948ab rejected from java.util.concurrent.ThreadPoolExecutor@59a46c25[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
怀疑压测并发量太大,线程池参数设置的不够合理导致的异常;
于是降低并发量重新去进行压测,试跑了一段时间,还比较平稳,逐渐加大并发量,发现现象日志仍不停的打印拒绝异常
观察机器内存逐渐升高,直到内存溢出,搜了下日志发现如下异常:
Caused by: java.lang.OutOfMemoryError: unable to create new native thread
内存溢出,无法创建更多的线程,此时去查看了一下机器当前的线程数:3W多了
使用 ulimit -u 命令查了下启用的最大线程数:30130
问题发现了,由于线程泄漏导致的内存溢出,接下里就得去排查代码了,看看哪里出问题了。
根据异常堆栈定位到如下代码,以下是伪代码,模拟的业务代码
业务service代码
- 定义了一个全局的线程池变量
- 在
execute
方法中,创建5个固定线程的线程池 - 通过线程池并发的执行5个任务
- 在 finally 里调用
shutdown
释放线程池资源
public class UserServiceImpl implements UserService {
public ExecutorService executorService;
@Override
public void execute() {
System.out.println("执行任务");
try {
executorService = Executors.newFixedThreadPool(5);
Task1 task1 = new Task1();
Task2 task2 = new Task2();
Task3 task3 = new Task3();
Task4 task4 = new Task4();
Task5 task5 = new Task5();
executorService.submit(task1);
executorService.submit(task2);
executorService.submit(task3);
executorService.submit(task4);
executorService.submit(task5);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭线程池");
executorService.shutdown();
}
}
}
客户端请求
- while循环模拟客户端请求,不停的调用业务service
public class TestController {
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// 模拟请求,不停的调用业务service
executorService.submit(userService::execute);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
不合理代码
- 乍一看就会发现,每次执行方法都创建了一个线程池,执行完再释放;这明显不大合理,频繁的创建销毁线程,造成性能损耗(违背了线程池的初衷:复用线程,减少创建销毁线程产生的不必要开销)
- 线程池变量是共享变量,并发情况下,请求A创建了一个线程池,请求B也创建了一个线程池,请求A释放掉的是线程B创建的线程池,请求A的线程池就留在了内存中,包括5个核心线程,并发请求高了,无法释放的线程逐渐累加,导致了问题的发生
问题复盘
- 调用shutdown为什么没有释放掉资源?
并发导致的,线程池是共享变量,请求A,请求B可能公用一个线程,请求A释放掉的是线程B创建的线程池,请求A的线程池就留在了内存中,包括5个核心线程 - 留在内存中的线程不会被垃圾回收吗?
当然不会,线程中的核心线程是活跃的,可以作为GC根节点,无法被GC回收 - 怎么会触发拒绝策略
还是并发问题,A,B两个请求共享一个线程池,只允许有5个线程,总共提交了10个任务,当然会触发拒绝策略
问题修复
在spring中定义一个线程池,业务代码中注入这个线程池
<bean id="poolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数,默认为1 -->
<property name="corePoolSize" value="5" />
<!-- 最大线程数,默认为Integer.MAX_VALUE -->
<property name="maxPoolSize" value="50" />
<!-- 队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE -->
<property name="queueCapacity" value="2000" />
<!-- 线程池维护线程所允许的空闲时间,默认为60s -->
<property name="keepAliveSeconds" value="100" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 -->
<property name="rejectedExecutionHandler">
<!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常 -->
<!-- CallerRunsPolicy:主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 -->
<!-- DiscardOldestPolicy:抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
<!-- DiscardPolicy:抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
事后总结
- 认真Code Review,规范代码编写
- 定义线程工厂,为不同线程池定义线程名