记录一次不合理的线程池使用

本文详细分析了一次由于线程池配置不当引发的内存溢出问题。在压力测试中,由于频繁创建和销毁线程池,导致线程泄漏,最终使得系统无法创建新的线程并抛出RejectedExecutionException。通过日志发现线程数过多,进一步引发了OutOfMemoryError。问题修复的关键在于合理配置线程池,避免线程泄漏,以及在代码中正确管理和关闭线程池。最后,提出了改进后的线程池配置和代码实践建议,以提高系统的稳定性和性能。
问题现象

压测持续了一段时间,很多接口响应逐渐超时,无法提供服务,于是停止压测,重启机器,开始排查问题

  • 观察线上日志,发现有大量线程池拒绝任务的抛出的异常
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,规范代码编写
  • 定义线程工厂,为不同线程池定义线程名
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值