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

news2024/12/25 0:04:41

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

今天上班把需求写完,出于学习(摸鱼)的心理上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();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}
`

我们从interruptIdleWorkers方法入手,这方法看上去最可疑,看到interruptIdleWorkers方法,这个方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。所以我们需要了解线程池里的线程是怎么处理中断的通知的。

我们点开worker对象,这个worker对象是线程池中实际运行的线程,所以我们直接看worker的run方法,中断通知肯定是在里面被处理了

*//WOrker的run方法里面直接调用的是这个方法* final void runWorker(Worker w) {     Thread wt = Thread.currentThread();     Runnable task = w.firstTask;     w.firstTask = null;     w.unlock(); *// allow interrupts*     boolean completedAbruptly = true;     try {         while (task != null || (task = getTask()) != null) {             w.lock();             *// If pool is stopping, ensure thread is interrupted;*             *// if not, ensure thread is not interrupted.  This*             *// requires a recheck in second case to deal with*             *// shutdownNow race while clearing interrupt*             if ((runStateAtLeast(ctl.get(), STOP) ||                  (Thread.interrupted() &&                   runStateAtLeast(ctl.get(), STOP))) &&                 !wt.isInterrupted())                 wt.interrupt();             try {                 beforeExecute(wt, task);                 Throwable thrown = null;                 try {                     task.run();                 } catch (RuntimeException x) {                     thrown = x; throw x;                 } catch (Error x) {                     thrown = x; throw x;                 } catch (Throwable x) {                     thrown = x; throw new Error(x);                 } finally {                     afterExecute(task, thrown);                 }             } finally {                 task = null;                 w.completedTasks++;                 w.unlock();             }         }         completedAbruptly = false;     } finally {         processWorkerExit(w, completedAbruptly);     } }

这个runwoker属于是线程池的核心方法了,相当的有意思,线程池能不断运作的原理就是这里,我们一点点看。

首先最外层用一个while循环套住,然后不断的调用gettask()方法不断从队列中取任务,假如拿不到任务或者任务执行发生异常(抛出异常了)那就属于异常情况,直接将completedAbruptly设置为true,并且进入异常的processWorkerExit流程。

我们看看gettask()方法,了解下啥时候可能会抛出异常:

`private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

for (;😉 {
        int c = ctl.get();
        int rs = runStateOf©;

// Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

int wc = workerCountOf©;

// Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount©)
                return null;
            continue;
        }

try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
`

这样很清楚了,抛去前面的大部分代码不看,这句代码解释了gettask的作用:

Runnable r = timed ?     workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take()

gettask就是从工作队列中取任务,但是前面还有个timed,这个timed的语义是这样的:如果allowCoreThreadTimeOut参数为true(一般为false)或者当前工作线程数超过核心线程数,那么使用队列的poll方法取任务,反之使用take方法。

这两个方法不是重点,重点是poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,我们调用线程的interrupt方法,毫无疑问会使线程当场抛出异常!

也就是说线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常。

那么线程池是在哪里处理这个异常的呢?我们看runwoker中的调用的processWorkerExit方法,说实话这个方法看着就像处理抛出异常的方法:

`private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn’t adjusted
        decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

tryTerminate();

int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf© >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}
`

我们可以看到,在这个方法里有一个很明显的workers.remove(w)方法,也就是在这里,这个w的变量,被移出了workers这个集合,导致worker对象不能到达gc root,于是workder对象顺理成章的变成了一个垃圾对象,被回收掉了。

然后等到worker中所有的worker都被移出works后,并且当前请求线程也完成后,线程池对象也成为了一个孤儿对象,没办法到达gc root,于是线程池对象也被gc掉了。写了挺长的篇幅,我小结一下:

  • 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常

  • 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了

  • 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放

总结

如果只是在局部方法中使用线程池,线程池对象不是bean的情况时,记得要合理的使用shutdown或者shutdownnow方法来释放线程和线程池对象,如果不使用,会造成线程池和线程对象的堆积。

来源:juejin.cn/post/7197424371991855159

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/828322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高忆管理:散户可以做空股票吗?散户如何做空?

在美国股市上,投资者能够进行做多操作,也能够进行做空操作,那么,在a股上,散户能够做空股票吗?散户如何做空?下面高忆管理为我们预备了相关内容,以供参考。 在a股市场上,散…

2023年最适合您业务的免费CRM——SaleSmartl

使用免费的CRM软件,例如SaleSmartly,可以协助您更好地发展您的业务,是提高生产力、减少错误、自动化重复性任务和享受交易机会的宝贵工具。您可以将多个渠道的客户信息集中在一个平台上,并允许员工访问,以便给他们分配…

VS+QT+VTK treeView树型结构模型加载隐藏实例

程序示例精选 VSQTVTK treeView树型结构模型加载隐藏实例 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<VSQTVTK treeView树型结构模型加载隐藏实例>>编写代码&#xff0c;代码…

gateway过滤器没生效,特殊原因

看这边文章的前提&#xff0c;你要会gateway&#xff0c;知道过滤器怎么配置&#xff1f; 直接来看过滤器&#xff0c;局部过滤器 再来看配置 请求路径 http://127.0.0.1:8080/appframework/services/catalog/catalogSpecials.json?pageindex1&pagesize10&pkidd98…

DAY56:单调栈(二)下一个最大元素Ⅱ(环形数组处理思路)

文章目录 思路写法1完整版环形数组处理&#xff1a;i取模&#xff0c;遍历两遍写法2完整版&#xff08;环形数组推荐写法&#xff09;debug测试&#xff1a;逻辑运算符短路特性result数组在栈口取元素&#xff0c;是否会覆盖原有数值&#xff1f; 给定一个循环数组 nums &#…

spring boot+thymeleaf+semantic ui 分页

参考&#xff1a; https://my.oschina.net/ayyao/blog/898041 后端 springboot 使用&#xff1a; com.github.pagehelper.PageInfo&#xff0c;作为分页对象 <!--引入分页插件--> <dependency><groupId>com.github.pagehelper</groupId><artifa…

[玩转AIGC]如何训练LLaMA2(模型训练、推理、代码讲解,并附可直接运行的kaggle连接)

目录 一、clone仓库二、数据集下载与处理1、数据集下载2、数据集标记化&#xff08;耗时较长&#xff09; 三、修改配置四、开始训练五、模型推理六、train.py训练代码讲解1、导包2、定义模型训练参数与相关设置3、加载模型配置4、迭代生成数据5、模型初始化6、设置自动混合精度…

Bug的严重等级和优先级别与分类

一、 Bug的严重等级定义&#xff1a; 1、 Blocker 即系统无法执行、崩溃或严重资源不足、应用模块无法启动或异常退出、无法测试、造成系统不稳定。 严重花屏内存泄漏 用户数据丢失或破坏系统崩溃/死机/冻结模块无法启动或异常退出严重的数值计算错误功能设计与需求严重不符其…

游戏发烧友、直播、视频创作者推荐,性能怪兽NUC12SNKI7 蝰蛇峡谷

在学校上课的时候一般习惯带着电脑去教室&#xff0c;选用轻薄的办公本或者 Mac整天带着到处跑。但从校园跨入社会后突然发现办公场景慢慢从移动办公转为固定场所的办公&#xff0c;公司因为保密等原因不能带私人电脑进公司&#xff0c;在家用办公本性能又不够用&#xff0c;甚…

UNet 系列:做医学图像分割的任何人,都必须要会使用 nnU-Net

UNet 系列 UNet下采样和上采样跳跃连接 UNet&#xff1a;多层级和多尺度的密集链接nnUNet集成模型预处理训练过程推理后处理4行命令使用 nnUNet 训练自己的医学图像分割模型 UNet 经典的卷积神经网络都很深&#xff0c;越深的卷积层越适合处理大目标的东西&#xff0c;而医学病…

《面试1v1》ElasticSearch基础

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

智能聊天助手,一步到位:ChatGLM-6B部署全攻略

目录 前言一、介绍二、使用方式2-1、安装2-2、代码调用&#xff1a; 使用如下代码时会自动下载模型。2-3、本地加载 三、运行总结 前言 ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;基于 General Language Model (GLM) 架构&#xff0c;具有 62 亿参数。结…

海外独立站怎么搭建?7个海外独立站搭建指南

在海外搭建独立站&#xff08;独立网站&#xff09;有几个关键步骤&#xff0c;以下是一个简要的指南&#xff1a; 选择域名和主机&#xff1a; 首先&#xff0c;选择一个适合你网站主题的域名。确保它简洁、易记&#xff0c;并且与你的品牌或内容相关联。 然后&#xff0c;…

ruby调试

如果下载 ruby-debug-ide gem install ruby-debug-ide vscode 下载 ruby扩展 1&#xff0c; ruby 2&#xff0c;修改launch.json

SSM(Vue3+ElementPlus+Axios+SSM前后端分离)【一】

文章目录 SSM(Vue3ElementPlusAxiosSSM前后端分离)--基础环境搭建【一】项目介绍项目功能/界面SSM 整合项目界面技术栈 项目基础环境搭建创建项目创建Maven 项目-提醒, 配置maven 的仓库镜像手动创建java 和test 相关目录引入项目依赖的jar 包给项目配置Tomcat启动Tomcat , 完成…

【LeetCode 75】第十八题(1732)找到最高海拔

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码运行结果&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目有些绕&#xff0c;简单来说就是给一个数组&#xff0c;让我们把数组累加起来&#xff0c;初始值为0&#xff0c;然后返回累…

如何用断言帮助检测错误

Java中的断言通过测试我们认为是正确的代码来帮助检测错误。 使用assert关键字进行断言。 其语法为&#xff1a; assert condition; 这里condition是一个布尔表达式&#xff0c;我们假定在程序执行时为真。 启用断言 默认情况下&#xff0c;断言在运行时被禁用并被忽略。…

手把手教你从零开始搭建个人博客

随着技术的进步和用户需求的变化&#xff0c;个人博客的形式和内容一直在不停地演变。为了给读者提供更丰富、有趣的阅读体验&#xff0c;搭建个人博客的网站一直在寻找更好的优化方法。所以现在出现了一批功能更完善的个人博客搭建软件&#xff0c;今天looklook就以HelpLook为…

【css】样式 +GASP

纯CSS实现四种方式文本反差色效果 mix-blend-mode: difference; clip-path&#xff1b; background-clip: text, padding-box outline 是绘制于元素周围的一条线&#xff0c;位于边框边缘的外围&#xff0c;可起到突出元素的作用。 css 样式之 filter 滤镜属性 用法与示例 使…

java版直播商城平台规划及常见的营销模式+电商源码+小程序+三级分销+二次开发 bbc

&#xfeff; 1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、R…