0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

线程数突增!领导说再这么写就gc掉我

马哥Linux运维 来源:马哥Linux运维 2023-08-22 15:35 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

前言

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

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

但是敏锐的我还是立刻意识到这个应用有不妥,因为线程数太多了,不符合我们一个正常健康的应用数量。熟练的打出cpu dump观察,首先看线程组名的概览。

114ffb40-402e-11ee-ac96-dac502259ad0.jpg

从线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的线程处于waiting状态并且无法释放,记接下来我们找几条线程的堆栈观察具体堆栈:

117816fc-402e-11ee-ac96-dac502259ad0.jpg

这个堆栈看上去很合理,线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。

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

我在idea搜索new ThreadPoolExecutor()得到的结果是这样的:

118d472a-402e-11ee-ac96-dac502259ad0.jpg

于是我陷入懵逼的状态,难道还有其他骚操作?

正在这时,一位不知名的郑网友发来一张截图:

11a77fe6-402e-11ee-ac96-dac502259ad0.jpg

好家伙!竟然是用new FixedTreadPool()整出来的。难怪我完全搜不到,因为用的new FixedTreadPool(),所以线程池中的线程名是默认的pool(又多了一个不使用Executors来创建线程池的理由)。

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

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

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

privatestaticvoidthreadDontGcDemo(){
ExecutorServiceexecutorService=Executors.newFixedThreadPool(10);
executorService.submit(()->{
System.out.println("111");
});
}

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

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

我简单的跑了个demo,循环的去new线程池,不调用shutdown方法,看看线程池能不能被回收

11c124f0-402e-11ee-ac96-dac502259ad0.jpg

打开java visual vm查看实时线程:

11ecc51a-402e-11ee-ac96-dac502259ad0.jpg

可以看到线程数和线程池都一直在增加,但是一直没有被回收,确实符合发生的问题状况,那么假如我在方法结束前调用shutdown方法呢,会不会回收线程池和线程呢?

简单写个demo结合jvisualvm验证下:

121f1574-402e-11ee-ac96-dac502259ad0.jpg

124f630a-402e-11ee-ac96-dac502259ad0.jpg

结果是线程和线程池都被回收了。也就是说,执行了shutdown的线程池最后会回收线程池和线程对象。

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

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

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

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

在stackoverflow上我找到了更准确的答案:

126e1340-402e-11ee-ac96-dac502259ad0.jpg

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方法的源码

publicvoidshutdown(){
finalReentrantLockmainLock=this.mainLock;
mainLock.lock();
try{
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown();//hookforScheduledThreadPoolExecutor
}finally{
mainLock.unlock();
}
tryTerminate();
}

privatevoidinterruptIdleWorkers(){
interruptIdleWorkers(false);
}

privatevoidinterruptIdleWorkers(booleanonlyOne){
finalReentrantLockmainLock=this.mainLock;
mainLock.lock();
try{
for(Workerw:workers){
Threadt=w.thread;
if(!t.isInterrupted()&&w.tryLock()){
try{
t.interrupt();
}catch(SecurityExceptionignore){
}finally{
w.unlock();
}
}
if(onlyOne)
break;
}
}finally{
mainLock.unlock();
}
}

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

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

//WOrker的run方法里面直接调用的是这个方法
finalvoidrunWorker(Workerw){
Threadwt=Thread.currentThread();
Runnabletask=w.firstTask;
w.firstTask=null;
w.unlock();//allowinterrupts
booleancompletedAbruptly=true;
try{
while(task!=null||(task=getTask())!=null){
w.lock();
//Ifpoolisstopping,ensurethreadisinterrupted;
//ifnot,ensurethreadisnotinterrupted.This
//requiresarecheckinsecondcasetodealwith
//shutdownNowracewhileclearinginterrupt
if((runStateAtLeast(ctl.get(),STOP)||
(Thread.interrupted()&&
runStateAtLeast(ctl.get(),STOP)))&&
!wt.isInterrupted())
wt.interrupt();
try{
beforeExecute(wt,task);
Throwablethrown=null;
try{
task.run();
}catch(RuntimeExceptionx){
thrown=x;throwx;
}catch(Errorx){
thrown=x;throwx;
}catch(Throwablex){
thrown=x;thrownewError(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()方法,了解下啥时候可能会抛出异常:

privateRunnablegetTask(){
booleantimedOut=false;//Didthelastpoll()timeout?

for(;;){
intc=ctl.get();
intrs=runStateOf(c);

//Checkifqueueemptyonlyifnecessary.
if(rs>=SHUTDOWN&&(rs>=STOP||workQueue.isEmpty())){
decrementWorkerCount();
returnnull;
}

intwc=workerCountOf(c);

//Areworkerssubjecttoculling?
booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;

if((wc>maximumPoolSize||(timed&&timedOut))
&&(wc>1||workQueue.isEmpty())){
if(compareAndDecrementWorkerCount(c))
returnnull;
continue;
}

try{
Runnabler=timed?
workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):
workQueue.take();
if(r!=null)
returnr;
timedOut=true;
}catch(InterruptedExceptionretry){
timedOut=false;
}
}
}

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

Runnabler=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方法,说实话这个方法看着就像处理抛出异常的方法:

privatevoidprocessWorkerExit(Workerw,booleancompletedAbruptly){
if(completedAbruptly)//Ifabrupt,thenworkerCountwasn'tadjusted
decrementWorkerCount();

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

tryTerminate();

intc=ctl.get();
if(runStateLessThan(c,STOP)){
if(!completedAbruptly){
intmin=allowCoreThreadTimeOut?0:corePoolSize;
if(min==0&&!workQueue.isEmpty())
min=1;
if(workerCountOf(c)>=min)
return;//replacementnotneeded
}
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方法来释放线程和线程池对象,如果不使用,会造成线程池和线程对象的堆积。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11216

    浏览量

    222858
  • 堆栈
    +关注

    关注

    0

    文章

    183

    浏览量

    20418
  • 线程
    +关注

    关注

    0

    文章

    508

    浏览量

    20753

原文标题:线程数突增!领导说再这么写就gc掉我

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    华为第七届5GC高层圆桌会议圆满落幕

    5G核心网峰会期间,由华为主办的第七届5GC高层圆桌会议成功举行。来自GSMA智库,以及中东、亚太、欧洲、拉丁美洲和非洲的电信运营商和行业客户的30多位高层领导齐聚一堂,共同探讨5G以及5G-A核心网的热点问题和未来发展趋势。
    的头像 发表于 12-01 13:59 307次阅读

    Linux多线程对比单线程的优势

    在Linux系统中,线程是操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运行单位。一个进程可以拥有多个线程,这些线程共享相同的内存空间和系统资源。
    发表于 12-01 06:11

    领导宣传一下产品,就这样....嘿嘿#搞笑

    电路
    银河电气
    发布于 :2025年11月12日 09:55:51

    切换线程后中断被屏蔽怎么解决?

    在将RT-thread移植到cortex-a7平台上,版本是rt-thread-5.1.0,参考的bsp是qemu-vexpress-a9。 目前可以启动到主线程上,具体是高优先级的timer线程
    发表于 09-29 07:48

    tcpip线程被mu0锁住导致网络线程无法使用怎么解决?

    各位好,使用rtthread开发STM32F407VGT6芯片,程序有多个线程,每个线程都会创建一个socket,建立tcp连接或者udp连接,现在出现一个问题,程序长时间运行有概率死机,但是没有
    发表于 09-29 06:41

    项目见证、提升效率丨热烈欢迎甲方领导专家组莅临司开展项目施工进度见证工作

    领导专家组首先对湖南泰德航空株洲生产基地车间进行实地考察,重点查看了司现场装配及测试车间工作环境,详细了解项目的施工进度,领导专家组对司"质量优先、精益求精"
    的头像 发表于 09-25 10:59 441次阅读
    项目见证、提升效率丨热烈欢迎甲方<b class='flag-5'>领导</b>专家组莅临<b class='flag-5'>我</b>司开展项目施工进度见证工作

    rtth studio中nano 如何创建动态线程

    有没有大佬,可以一下为什么静态线程可以正常使用,动态线程怎么也使用不了。 具体需要什么配置才能使用动态线程创建。谢谢!
    发表于 09-11 06:01

    rt-thread线程调用rt_thread_mdelay延时后恢复运行时thread会变,导致mutex释放失败,怎么解决?

    代码片段如下: 运行结果: 可以看到,在线程延时后,恢复运行时,用rt_thread_self函数获取到的值与没有延时时获取到的值不一样了,导致mutex无法释放,其它等待这个mutex的线程都无法运行了,应该怎么处理这个
    发表于 09-09 08:06

    电机的极什么意思?2极,4极,6极,8极的区别是什么?

    前两天有一个客户问我,电机的极是什么意思,不同极的区别是什么,虽然是做无刷驱动方案的,但是这方面也可以给大家科普一下。首先,电机的极
    的头像 发表于 08-22 18:07 7731次阅读
    电机的极<b class='flag-5'>数</b>什么意思?2极,4极,6极,8极的区别是什么?

    rtt studio中nano 如何创建动态线程

    有没有大佬,可以一下为什么静态线程可以正常使用,动态线程怎么也使用不了。 具体需要什么配置才能使用动态线程创建。谢谢!
    发表于 08-22 06:19

    UVC+MSC实现中MSC线程未运行的原因?

    正在尝试使用 EZUSB 运行 UVC + MSC。有以下内容。但看起来只有 UVC 线程在运行,而 MSC 没有运行。fw 不响应 MSC 命令。确保 LPM 已被禁用,只是为
    发表于 07-16 07:08

    开环霍尔电流传感芯片GC1868产品介绍

    GC1868 系列是一款开环霍尔电流传感芯片,其具有高精度、高带宽、快速响应、高线性度、低温度漂移等优点。GC1868提供 0~50A 电流检测范围。GC1868 为高性能电流传感器领域提供了新的解决方案。
    的头像 发表于 05-14 11:18 686次阅读
    开环霍尔电流传感芯片<b class='flag-5'>GC</b>1868产品介绍

    掌握这六点,原来学习电和模电这么简单!

    对于电模电这两门功课,在大学课堂中有的学校先后模有的先模后,当然也有的同时开设。其实爱好电子技术的小伙伴们,给大家的建议是先学习模电再学习
    的头像 发表于 03-19 19:33 3645次阅读
    掌握这六点,原来学习<b class='flag-5'>数</b>电和模电<b class='flag-5'>这么</b>简单!

    郑州市领导莅临中科驭调研

    郑州市工业和信息化局党组成员、副局长曹永涛一行人来访中科驭,双方共同探讨在算力产业方向的合作。
    的头像 发表于 03-11 09:15 1014次阅读

    GC5016根据datasheet这个配置对应的不是实数输入,为什么?

    。。。。。 PS:滤波器系数部分用的是原始的8bit I+8 bit Q进,16 bit实数出的配置。这会不会也有影响??如何将滤波器Bypass???
    发表于 01-22 07:56