线程数突增!领导说再这么写就gc掉我
前语
咱们好,我是魔性的茶叶,今日给咱们分享一个线上问题引出的一次考虑,进程比较长,可是挺有意思。
今日上班把需求写完,出于学习(摸鱼)的心理上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上我找到了更准确的答案:stackoverflow.com/questions/2…
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(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便是从工作行列中取使命,可是前面还有个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(c) >= 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办法来开释线程和线程池目标,假如不运用,会形成线程池和线程目标的堆积。