前言
那么為啥線程池里面的線程和線程池都沒釋放呢。
那么現(xiàn)在問題就轉(zhuǎn)為線程對(duì)象是在什么時(shí)候gc。
最后總結(jié)
前言
今天給大家分享一個(gè)線上問題引出的一次思考,過程比較長(zhǎng),但是挺有意思。
今天上班把需求寫完,出于學(xué)習(xí)(摸魚)的心理上skywalking看看,突然發(fā)現(xiàn)我們的一個(gè)應(yīng)用,應(yīng)用內(nèi)線程數(shù)超過900條,接近1000條,但是cpu并沒有高漲,內(nèi)存也不算高峰。
但是敏銳的我還是立刻意識(shí)到這個(gè)應(yīng)用有不妥,因?yàn)榫€程數(shù)太多了,不符合我們一個(gè)正常健康的應(yīng)用數(shù)量。熟練的打出cpu dump觀察,首先看線程組名的概覽。
從線程分組看,pool名開頭線程占616條,而且waiting狀態(tài)也是616條,這個(gè)點(diǎn)就非常可疑了,我斷定就是這個(gè)pool開頭線程池導(dǎo)致的問題。我們先排查為何這個(gè)線程池中會(huì)有600+的線程處于waiting狀態(tài)并且無法釋放,記接下來我們找?guī)讞l線程的堆棧觀察具體堆棧:
這個(gè)堆??瓷先ズ芎侠恚€程在線程池中不斷的循環(huán)獲取任務(wù),因?yàn)楂@取不到任務(wù)所以進(jìn)入了waiting狀態(tài),等待著有任務(wù)后被喚醒。
看上去不只一個(gè)線程池,并且這些線程池的名字居然是一樣的,我大膽的猜測(cè)一下,是不斷的創(chuàng)建同樣的線程池,但是線程池?zé)o法被回收導(dǎo)致的線程數(shù),所以接下來我們要分析兩個(gè)問題,首先這個(gè)線程池在代碼里是哪個(gè)線程池,第二這個(gè)線程池是怎么被創(chuàng)建的?為啥釋放不了?
我在idea搜索new ThreadPoolExecutor()得到的結(jié)果是這樣的:
于是我陷入懵逼的狀態(tài),難道還有其他騷操作?
正在這時(shí),一位不知名的鄭網(wǎng)友發(fā)來一張截圖:
好家伙!竟然是用new FixedTreadPool()整出來的。難怪我完全搜不到,因?yàn)橛玫膎ew FixedTreadPool(),所以線程池中的線程名是默認(rèn)的pool(又多了一個(gè)不使用Executors來創(chuàng)建線程池的理由)。
然后我迫不及die的打開代碼,試圖找到罪魁禍?zhǔn)?,結(jié)果發(fā)現(xiàn)作者居然是我自己。這是另一個(gè)驚喜,驚嚇的驚。
冷靜下來后我梳理一遍代碼,這個(gè)接口是我兩年前寫的,主要是功能是統(tǒng)計(jì)用戶的錢包每個(gè)月的流水,因?yàn)閾?dān)心統(tǒng)計(jì)比較慢,所以使用了線程池,做了批量的處理,沒想到居然導(dǎo)致了線程數(shù)過高,雖然沒有導(dǎo)致事故,但是確實(shí)是潛在的隱患,現(xiàn)在沒出事不代表以后不會(huì)出事。
去掉多余業(yè)務(wù)邏輯,我簡(jiǎn)單的還原一個(gè)代碼給大家看,還原現(xiàn)場(chǎng):
private?static?void?threadDontGcDemo(){
????????ExecutorService?executorService?=?Executors.newFixedThreadPool(10); ????????executorService.submit(()?->?{ ????????????System.out.println("111"); ????????}); ????}
那么為啥線程池里面的線程和線程池都沒釋放呢。
難道是因?yàn)闆]有調(diào)用shutdown?我大概能理解我兩年前當(dāng)時(shí)為啥不調(diào)用shutdown,是因?yàn)楫?dāng)初我覺得接口跑完,方法走到結(jié)束,理論上棧幀出棧,局部變量應(yīng)該都銷毀了,按理說executorService這個(gè)變量應(yīng)該直接GG了,那么按理說我是不用調(diào)用shutdown方法的。
我簡(jiǎn)單的跑了個(gè)demo,循環(huán)的去new線程池,不調(diào)用shutdown方法,看看線程池能不能被回收
打開java visual vm查看實(shí)時(shí)線程:
可以看到線程數(shù)和線程池都一直在增加,但是一直沒有被回收,確實(shí)符合發(fā)生的問題狀況,那么假如我在方法結(jié)束前調(diào)用shutdown方法呢,會(huì)不會(huì)回收線程池和線程呢?
簡(jiǎn)單寫個(gè)demo結(jié)合jvisualvm驗(yàn)證下:
結(jié)果是線程和線程池都被回收了。也就是說,執(zhí)行了shutdown的線程池最后會(huì)回收線程池和線程對(duì)象。
我們知道,一個(gè)對(duì)象能不能回收,是看它到gc root之間有沒有可達(dá)路徑,線程池不能回收說明到達(dá)線程池的gc root還是有可達(dá)路徑的。這里講個(gè)冷知識(shí),這里的線程池的gc root是線程,具體的gc路徑是thread->workers->線程池。
線程對(duì)象是線程池的gc root,假如線程對(duì)象能被gc,那么線程池對(duì)象肯定也能被gc掉(因?yàn)榫€程池對(duì)象已經(jīng)沒有到gc root的可達(dá)路徑了)。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
那么現(xiàn)在問題就轉(zhuǎn)為線程對(duì)象是在什么時(shí)候gc。
鄭網(wǎng)友給了一個(gè)粗淺但是合理的解釋,線程對(duì)象肯定不是在運(yùn)行中的時(shí)候被回收的,因?yàn)閖vm肯定不可能去回收一條在運(yùn)行中的線程,至少runnalbe狀態(tài)的線程jvm不可能去回收。
在stackoverflow上我找到了更準(zhǔn)確的答案:
A running thread is considered a so called garbage collection root and is one of those things keeping stuff from being garbage collected。
這句話的意思是,一條正在運(yùn)行的線程是gc root,注意,是正在運(yùn)行,這個(gè)正在運(yùn)行我先透露下,即使是waiting狀態(tài),也算正在運(yùn)行。這個(gè)回答的整體的意思是,運(yùn)行的線程是gc root,但是非運(yùn)行的線程不是gc root(可以被回收)。
現(xiàn)在比較清楚了,線程池和線程被回收的關(guān)鍵就在于線程能不能被回收,那么回到原來的起點(diǎn),為何調(diào)用線程池的shutdown方法能夠?qū)е戮€程和線程池被回收呢?難道是shutdown方法把線程變成了非運(yùn)行狀態(tài)嗎?
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方法,這個(gè)方法里面主要就做了一件事,遍歷當(dāng)前線程池中的線程,并且調(diào)用線程的interrupt()方法,通知線程中斷,也就是說shutdown方法只是去遍歷所有線程池中的線程,然后通知線程中斷。所以我們需要了解線程池里的線程是怎么處理中斷的通知的。
我們點(diǎn)開worker對(duì)象,這個(gè)worker對(duì)象是線程池中實(shí)際運(yùn)行的線程,所以我們直接看worker的run方法,中斷通知肯定是在里面被處理了
//WOrker的run方法里面直接調(diào)用的是這個(gè)方法 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); ????????} }
這個(gè)runwoker屬于是線程池的核心方法了,相當(dāng)?shù)挠幸馑?,線程池能不斷運(yùn)作的原理就是這里,我們一點(diǎn)點(diǎn)看。
首先最外層用一個(gè)while循環(huán)套住,然后不斷的調(diào)用gettask()方法不斷從隊(duì)列中取任務(wù),假如拿不到任務(wù)或者任務(wù)執(zhí)行發(fā)生異常(拋出異常了)那就屬于異常情況,直接將completedAbruptly 設(shè)置為true,并且進(jìn)入異常的processWorkerExit流程。
我們看看gettask()方法,了解下啥時(shí)候可能會(huì)拋出異常:
private?Runnable?getTask()?{
????????boolean?timedOut?=?false;?//?Did?the?last?poll()?time?out? ????????for?(;;)?{ ????????????int?c?=?ctl.get(); ????????????int?rs?=?runStateOf(c); ????????????//?Check?if?queue?empty?only?if?necessary. ????????????if?(rs?>=?SHUTDOWN?&&?(rs?>=?STOP?||?workQueue.isEmpty()))?{ ????????????????decrementWorkerCount(); ????????????????return?null; ????????????} ????????????int?wc?=?workerCountOf(c); ????????????//?Are?workers?subject?to?culling? ????????????boolean?timed?=?allowCoreThreadTimeOut?||?wc?>?corePoolSize; ????????????if?((wc?>?maximumPoolSize?||?(timed?&&?timedOut)) ????????????????&&?(wc?>?1?||?workQueue.isEmpty()))?{ ????????????????if?(compareAndDecrementWorkerCount(c)) ????????????????????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就是從工作隊(duì)列中取任務(wù),但是前面還有個(gè)timed,這個(gè)timed的語義是這樣的:如果allowCoreThreadTimeOut參數(shù)為true(一般為false)或者當(dāng)前工作線程數(shù)超過核心線程數(shù),那么使用隊(duì)列的poll方法取任務(wù),反之使用take方法。
這兩個(gè)方法不是重點(diǎn),重點(diǎn)是poll方法和take方法都會(huì)讓當(dāng)前線程進(jìn)入time_waiting或者waiting狀態(tài)。而當(dāng)線程處于在等待狀態(tài)的時(shí)候,我們調(diào)用線程的interrupt方法,毫無疑問會(huì)使線程當(dāng)場(chǎng)拋出異常!
也就是說線程池的shutdownnow方法調(diào)用interruptIdleWorkers去對(duì)線程對(duì)象interrupt是為了讓處于waiting或者是time_waiting的線程拋出異常。
那么線程池是在哪里處理這個(gè)異常的呢?我們看runwoker中的調(diào)用的processWorkerExit方法,說實(shí)話這個(gè)方法看著就像處理拋出異常的方法:
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(c)?>=?min) ????????????????????return;?//?replacement?not?needed ????????????} ????????????addWorker(null,?false); ????????} }
我們可以看到,在這個(gè)方法里有一個(gè)很明顯的 workers.remove(w)方法,也就是在這里,這個(gè)w的變量,被移出了workers這個(gè)集合,導(dǎo)致worker對(duì)象不能到達(dá)gc root,于是workder對(duì)象順理成章的變成了一個(gè)垃圾對(duì)象,被回收掉了。
然后等到worker中所有的worker都被移出works后,并且當(dāng)前請(qǐng)求線程也完成后,線程池對(duì)象也成為了一個(gè)孤兒對(duì)象,沒辦法到達(dá)gc root,于是線程池對(duì)象也被gc掉了。寫了挺長(zhǎng)的篇幅,我小結(jié)一下:
線程池調(diào)用shutdownnow方法是為了調(diào)用worker對(duì)象的interrupt方法,來打斷那些沉睡中的線程(waiting或者time_waiting狀態(tài)),使其拋出異常
線程池會(huì)把拋出異常的worker對(duì)象從workers集合中移除引用,此時(shí)被移除的worker對(duì)象因?yàn)闆]有到達(dá)gc root的路徑已經(jīng)可以被gc掉了
等到workers對(duì)象空了,并且當(dāng)前tomcat線程也結(jié)束,此時(shí)線程池對(duì)象也可以被gc掉,整個(gè)線程池對(duì)象成功釋放
最后總結(jié)
如果只是在局部方法中使用線程池,線程池對(duì)象不是bean的情況時(shí),記得要合理的使用shutdown或者shutdownnow方法來釋放線程和線程池對(duì)象,如果不使用,會(huì)造成線程池和線程對(duì)象的堆積。
編輯:黃飛
?
評(píng)論