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

前语

咱们好,我是魔性的茶叶,今日给咱们分享一个线上问题引出的一次考虑,进程比较长,可是挺有意思。

今日上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现咱们的一个运用,运用内线程数超越900条,接近1000条,可是cpu并没有高涨,内存也不算高峰。可是敏锐的我仍是立刻意识到这个运用有不当,由于线程数太多了,不契合咱们一个正常健康的运用数量。熟练的打出cpu dump调查,首先看线程组名的概览。

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

从线程分组看,pool名最初线程占616条,而且waiting状况也是616条,这个点就十分可疑了,我断定便是这个pool最初线程池导致的问题。咱们先排查为何这个线程池中会有600+的线程处于waiting状况而且无法开释,记接下来咱们找几条线程的仓库调查详细仓库:

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

这个仓库看上去很合理,线程在线程池中不断的循环获取使命,由于获取不到使命所以进入了waiting状况,等待着有使命后被唤醒。

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

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

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

所以我堕入懵逼的状况,莫非还有其他骚操作?

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

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

好家伙!竟然是用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办法,看看线程池能不能被收回

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

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

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

能够看到线程数和线程池都一向在增加,可是一向没有被收回,确实契合产生的问题状况,那么假设我在办法完毕前调用shutdown办法呢,会不会收回线程池和线程呢?

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

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

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

成果是线程和线程池都被收回了。也便是说,履行了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…

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

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掉了。

写了挺长的篇幅,我小结一下:

  1. 线程池调用shutdownnow办法是为了调用worker目标的interrupt办法,来打断那些沉睡中的线程(waiting或许time_waiting状况),使其抛出反常
  2. 线程池会把抛出反常的worker目标从workers调集中移除引用,此刻被移除的worker目标由于没有抵达gc root的途径已经能够被gc掉了
  3. 比及workers目标空了,而且当时tomcat线程也完毕,此刻线程池目标也能够被gc掉,整个线程池目标成功开释

最终总结:

假如仅仅在局部办法中运用线程池,线程池目标不是bean的状况时,记得要合理的运用shutdown或许shutdownnow办法来开释线程和线程池目标,假如不运用,会形成线程池和线程目标的堆积。