线程数突增!领导:谁再这么写就滚蛋!

文章讲述了作者在检查应用性能时发现线程数过多但资源占用不高,通过分析发现是由于频繁创建未正确回收的固定线程池导致。作者深入探究了线程池的工作原理,尤其是shutdown方法如何促使等待中的线程抛出异常并回收资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。

下面是正文。

今天上班把需求写完,出于学习(摸鱼)的心理上 skywalking 看看,突然发现我们的一个应用,应用内线程数超过 900 条,接近 1000 条,但是 cpu 并没有高涨,内存也不算高峰。

但是敏锐的我还是立刻意识到这个应用有不妥,因为线程数太多了,不符合我们一个正常健康的应用数量。熟练的打出 cpu dump 观察,首先看线程组名的概览。
在这里插入图片描述
从线程分组看,pool 名开头线程占 616 条,而且 waiting 状态也是 616 条,这个点就非常可疑了,我断定就是这个 pool 开头线程池导致的问题。我们先排查为何这个线程池中会有 600+的线程处于 waiting 状态并且无法释放,记接下来我们找几条线程的堆栈观察具体堆栈:
在这里插入图片描述
这个堆栈看上去很合理,线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了 waiting 状态,等待着有任务后被唤醒。

看上去不只一个线程池,并且这些线程池的名字居然是一样的,我大胆的猜测一下,是不断的创建同样的线程池,但是线程池无法被回收导致的线程数,所以接下来我们要分析两个问题,首先这个线程池在代码里是哪个线程池,第二这个线程池是怎么被创建的?为啥释放不了?

我在 idea 搜索new ThreadPoolExecutor()得到的结果是这样的:
在这里插入图片描述
于是我陷入懵逼的状态,难道还有其他骚操作?

正在这时,一位不知名的郑网友发来一张截图:
在这里插入图片描述
好家伙!竟然是用new FixedTreadPool()整出来的。难怪我完全搜不到,因为用的new FixedTreadPool(),所以线程池中的线程名是默认的 pool(又多了一个不使用 Executors 来创建线程池的理由)。

然后我迫不及 die 的打开代码,试图找到罪魁祸首,结果发现作者居然是我自己。这是另一个惊喜,惊吓的惊。

冷静下来后我梳理一遍代码,这个接口是我两年前写的,主要是功能是统计用户的钱包每个月的流水,因为担心统计比较慢,所以使用了线程池,做了批量的处理,没想到居然导致了线程数过高,虽然没有导致事故,但是确实是潜在的隐患,现在没出事不代表以后不会出事。

去掉多余业务逻辑,我简单的还原一个代码给大家看,还原现场:

private static void threadDontGcDemo(){
   
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    executorService.submit(() -> {
   
        System.out.println("111");
    });
}

那么为啥线程池里面的线程和线程池都没释放呢

难道是因为没有调用 shutdown?我大概能理解我两年前当时为啥不调用 shutdown,是因为当初我觉得接口跑完,方法走到结束,理论上栈帧出栈,局部变量应该都销毁了,按理说executorService这个变量应该直接 GG 了,那么按理说我是不用调用 shutdown 方法的。

我简单的跑了个 demo,循环的去 new 线程池,不调用 shutdown 方法,看看线程池能不能被回收
在这里插入图片描述
打开java visual vm查看实时线程:
在这里插入图片描述
可以看到线程数和线程池都一直在增加,但是一直没有被回收,确实符合发生的问题状况,那么假如我在方法结束前调用 shutdown 方法呢,会不会回收线程池和线程呢?

简单写个 demo 结合 jvisualvm 验证下:
在这里插入图片描述
在这里插入图片描述
结果是线程和线程池都被回收了。也就是说,执行了 shutdown 的线程池最后会回收线程池和线程对象。

我们知道,一个对象能不能回收,是看它到 gc root 之间有没有可达路径,线程池不能回收说明到达线程池的 gc root 还是有可达路径的。这里讲个冷知识,这里的线程池的 gc root 是线程,具体的 gc 路径是thread->workers->线程池。

线程对象是线程池的 gc root,假如线程对象能被 gc,那么线程池对象肯定也能被 gc 掉(因为线程池对象已经没有到 gc root 的可达路径了)。

那么现在问题就转为线程对象是在什么时候 gc

郑网友给了一个粗浅但是合理的解释,线程对象肯定不是在运行中的时候被回收的,因为 jvm 肯定不可能去回收一条在运行中的线程,至少 runnalbe 状态的线程 jvm 不可能去回收。

在 stackoverflow 上我找到了更准确的答案:
在这里插入图片描述

A running thread is considered a so called garbage collection root and is one of those things keeping stuff from being garbage collected

这句话的意思是,一条正在运行的线程是 gc root,注意,是正在运行,这个正在运行我先透露下,即使是 waiting 状态,也算正在运行。这个回答的整体的意思是,运行的线程是 gc root,但是非运行的线程不是 gc root(可以被回收)。

现在比较清楚了,线程池和线程被回收的关键就在于线程能不能被回收,那么回到原来的起点,为何调用线程池的 shutdown 方法能够导致线程和线程池被回收呢?难道是 shutdown 方法把线程变成了非运行状态吗?

talk is cheap,show me the code

我们直接看看线程池的 shutdown 方法的源码

public void shutdown() {
   
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
   
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyufeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值