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

    文章

    10456

    浏览量

    206590
  • 堆栈
    +关注

    关注

    0

    文章

    171

    浏览量

    19534
  • 线程
    +关注

    关注

    0

    文章

    490

    浏览量

    19503

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

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

收藏 人收藏

    评论

    相关推荐

    什么是动态线程池?动态线程池的简单实现思路

    因此,动态可监控线程池一种针对以上痛点开发的线程池管理工具。主要可实现功能有:提供对 Spring 应用内线程池实例的全局管控、应用运行时动态变更线程池参数以及
    的头像 发表于 02-28 10:42 185次阅读

    线程池的创建方式有几种

    线程池是一种用于管理和调度线程的技术,能够有效地提高系统的性能和资源利用率。它通过预先创建一组线程并维护一个工作队列,将任务提交给线程池来处理,从而减少
    的头像 发表于 12-04 16:52 420次阅读

    核心线程数和最大线程数怎么设置

    核心线程数和最大线程数是Java线程池中重要的参数,用来控制线程池中线程的数量和行为。正确地设置这两个参数可以优化系统的性能和资源利用率。本
    的头像 发表于 12-01 13:50 4132次阅读

    .NET8为什么要引入Non-GC Heap这种机制呢?

    .NET8里面JIT引入了一个新的机制,叫做Non-GC Heap。JIT可以确保相关对象分配在Non-GC Heap上,该堆像其名称一样,不受GC管理。
    的头像 发表于 11-28 10:38 273次阅读

    如何查看一个线程的ID

    1.什么是线程? linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念。 1.1线程是怎样描述的?
    的头像 发表于 11-13 14:38 456次阅读
    如何查看一个<b class='flag-5'>线程</b>的ID

    Linux线程线程与异步编程、协程与异步介绍

    协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换。 协程和用户态
    的头像 发表于 11-11 11:35 431次阅读
    Linux<b class='flag-5'>线程</b>、<b class='flag-5'>线程</b>与异步编程、协程与异步介绍

    线程池的基本概念

    池? 呃呃,我这么问就很奇怪,因为线程池是什么我都没说,怎么会知道为什么会有线程池呢?所以我打算带大家去思考一个场景: 当我们的程序中:有一批任务到来时候(通常该任务都是从网络来的),我们就会创建一堆
    的头像 发表于 11-10 16:37 269次阅读
    <b class='flag-5'>线程</b>池的基本概念

    线程池基本概念与原理

    一、线程池基本概念与原理 1.1 线程池概念及优势 C++线程池简介 线程池是一种并发编程技术,它能有效地管理并发的线程、减少资源占用和提高
    的头像 发表于 11-10 10:24 286次阅读

    微软发布Azure Bpsv2可突增虚拟机 搭载Ampere Altra系列处理器

    近日,微软发布了最新的 Azure 虚拟机——Bpsv2 可突增虚拟机正式版。与 Dpsv5、Epsv5 实例相同,Bpsv2 可突增虚拟机也搭载了 Ampere Altra 系列处理器。B 系列
    的头像 发表于 10-31 13:12 460次阅读
    微软发布Azure Bpsv2可<b class='flag-5'>突增</b>虚拟机 搭载Ampere Altra系列处理器

    修改线程初始化程序名部分,stackoverflow为什么会消失?

    可能会stackoverflow,不断的寻找报错的位置以后发现,线程里的内容全部删除,加了一个延时函数。让线程不断运行,还是会stackoverflow。但神奇的是,
    发表于 08-20 11:40

    线程池的线程怎么释放

    线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的
    发表于 07-31 10:49 1258次阅读
    <b class='flag-5'>线程</b>池的<b class='flag-5'>线程</b>怎么释放

    如何用C++实现一个线程池呢?

    C++线程池是一种多线程管理模型,把线程分成任务执行和线程调度两部分。
    发表于 06-08 14:53 802次阅读
    如何用C++实现一个<b class='flag-5'>线程</b>池呢?

    核心线程数和最大线程数区别

    核心线程数和最大线程数区别 核心线程数是线程池中一直存在的线程数,不会被回收。最大线程数是
    的头像 发表于 06-01 09:33 6117次阅读

    什么是线程安全?如何理解线程安全?

    在多线程编程中,线程安全是必须要考虑的因素。
    的头像 发表于 05-30 14:33 1512次阅读
    什么是<b class='flag-5'>线程</b>安全?如何理解<b class='flag-5'>线程</b>安全?

    程序运行过程中输入PS后很多线程没有了,请问是什么原因?

    程序运行过程中,本来运行着很多线程,输入ps后,可以正常显示,在finsh中运行了个函数,置位了一个标志位,是个全局的,运行完了以后,输入ps,很多线程就都没有了,包括连tshel
    发表于 05-11 09:52