面试官给我讲讲线程池(下)

前景回顾

大家好呀,我是小张,我们在上一篇中 面试官:给我讲讲线程池(中),通过线程池的常见API作为切入点,分析了execute方法的源码,其中学到了Doug Lea老爷子把一个变量拆成两个变量的骚操作、无锁开发(CAS)、如何并发控制线程池状态等,以及最后通过源码阅读给大家带来了我认为高效阅读源码的方法论。

如果还没有读过上一篇的小伙伴请先阅读上一篇,在本篇中将会围绕以下几个API的源码阅读中展开。

  • processWorkerExit

  • shutdown

  • shutdownNow

  • awaitTermination

工作线程退出

我们继续上一篇中worker的runWorker()的worker线程结束开始讲,下面我将代码中其他流程简化只展示了核心代码。

final void runWorker(Worker w) {
       boolean completedAbruptly = true;
       try {
           while (task != null || (task = getTask()) != null) {
               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);
      }
  }

在上一篇中我们通过分析只有当getTask()方法返回null的时候,while循环才会结束,才会进入processWorkerExit()方法中。

其中getTask()会在线程池状态改变时(变为非Running),或者当获取任务超时且当前工作线程数大于核心线程数返回null。

那么理所应当的processWorkerExit方法就是执行工作线程的清理工作,下面将通过源码带大家看看是如何进行工作线程的清理的。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
   // 如果非正常结束,将工作线程数-1
       if (completedAbruptly)
           decrementWorkerCount();
// 获取锁,该锁主要为了操作工作线程集合
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           // 汇总该worker线程完成的任务数
           completedTaskCount += w.completedTasks;
           // 从worker集合中删除
           workers.remove(w);
      } finally {
           mainLock.unlock();
      }

   // 因为线程池的状态不知道在何时会被修改,所以需要尝试去结束线程池
       tryTerminate();

       int c = ctl.get();
    // 当Running或SHUTDOWN状态
       if (runStateLessThan(c, STOP)) {
           // 如果非异常中断,计算最小应存活工作线程数
           if (!completedAbruptly) {
               // 如允许核心线程超时,最小值0 否则为核心线程数
               int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
              // 当最小值为0时,任务队列还有任务时,至少还需要一个线程去处理
               if (min == 0 && ! workQueue.isEmpty())
                   min = 1;
               // 判断当前工作线程数是否小于最小值,如是直接返回
               if (workerCountOf(c) >= min)
                   return; // replacement not needed
          }
           // 如果异常中断或当前工作线程数小于最小值,需要重新添加工作线程
           addWorker(null, false);
      }
  }

通过上方分析,我们可以推断出工作线程清理过程是先清理worker集合,再会执行尝试结束线程池,然后如果线程池状态处于Running或SHUTDOWN时,将会计算最小工作线程数min,保证当前工作线程数总是有min个存活。有小伙伴可能会好奇,为什么会去尝试去结束线程池,其实道理很简单,线程池何时被结束对于程序是未知的,所以需要在每个线程经过的地方来去判断状态是否变化。

关闭线程池

在实际场景中,关闭线程池有两种方式,一种调用shutdown一种调用shutdownNow,两者区别在于前者线程池还可以继续处理线程池中任务,后者将会中断所有任务并将未执行的任务返回。下面我将通过对比的方式,让大家更清楚的阅读其中的不同。

通过上图红框中,首先两者要改变的状态不同,一个要改变为SHUTDOWN一个要改变为STOP状态。其次一个要中断空闲线程,一个要中断所有线程。如果有小伙伴不了解这里的线程池状态,请到上一篇文章阅读。

那么线程池是如何被改变状态,工作线程又是如何被中断的呢,阅读下面的源码将豁然开朗。

advanceRunState

// 推进线程池状态
private void advanceRunState(int targetState) {
       for (;;) {
           int c = ctl.get();
           if (
             // 如果当前状态大于等于目标状态则break
             runStateAtLeast(c, targetState) ||
             // 如果当前状态小于目标状态则使用CAS尝试去修改,修改成功则break
               ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
               break;
      }
  }

上方的代码非常简单,在多线程中并发中,线程池的状态随时可能发生变化。所以这里需要死循环通过CAS的方式去修改线程池状态,保证原子性。

interruptIdleWorkers

private void interruptIdleWorkers() {
       interruptIdleWorkers(false);
  }
​
private void interruptIdleWorkers(boolean onlyOne) {
// 我们需要操作worker集合,所以上锁
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
         // 遍历worker集合
           for (Worker w : workers) {
               Thread t = w.thread;
             
               if (
                 // 线程非中断
                 !t.isInterrupted() 
                 // 尝试获取worker的锁,判断worker是否空闲
                 && w.tryLock()) {
                   try {
                     // 空闲worker,直接使用中断
                       t.interrupt();
                  } catch (SecurityException ignore) {
                  } finally {
                     // 释放worker的锁
                       w.unlock();
                  }
              }
             // 如只中断一个,直接break
               if (onlyOne)
                   break;
          }
      } finally {
         // 释放操作worker集合的锁
           mainLock.unlock();
      }
  }

阅读下来,该方法阅读并不困难,但是在worker是否空闲处,使用了tryLock方法去判断worker是否空闲,小伙伴们可能会疑问为什么tryLock成功就是空闲的worker线程呢?这里我们就需要结合runWorker()方法去看。

如上图,我们会发现工作线程池要么处于等待①处返回task任务,要么处于③执行任务。我们会发现在获取task返回时,就会进入②,这里不熟悉AQS的小伙伴可以把②处认为是标记worker线程被占用。任务执行完毕将会调用④方法,标记释放worker线程。所以我们可以得知,只要尝试占用成功的都是没有获取到task任务的worker线程,换言之这些线程就是空闲线程。

interruptWorkers

根据字面意思,该方法作用是中断所有工作线程池,不管是否正在执行任务。

private void interruptWorkers() {
   // 获取占用worker集合的锁
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           // 遍历worker集合
           for (Worker w : workers)
               // 直接中断
               w.interruptIfStarted();
      } finally {
           // 释放锁
           mainLock.unlock();
      }
}
​
void interruptIfStarted() {
           Thread t;
           if (
               // 0为未被占用、1为被占用
               getState() >= 0 
               && 
               // 线程不会空且未被标记为中断,则调用线程的中断
              (t = thread) != null && !t.isInterrupted()) {
               try {
                   // 线程中断
                   t.interrupt();
              } catch (SecurityException ignore) {
              }
          }
}

我们会发现中断所有线程和中断空闲线程的方法中,唯一不同的是这个方法使用了getState()大于等于0来判断线程状态。

那么为什么是大于等于0呢,我们回到worker中的lock和unlock方法中。

    public void lock()       { acquire(1); }
       public boolean tryLock() { return tryAcquire(1); }
       public void unlock()     { release(1); }
​
Worker(Runnable firstTask) {
           setState(-1);
           this.firstTask = firstTask;
           this.thread = getThreadFactory().newThread(this);
      }

熟悉AQS的小伙伴肯定会明白,调用lock方法就会将状态改为1,unlock方法将会把状态改为0。我们回到worker类的构造函数中,会发现第一条就是先把状态设置为-1,到这里是不是豁然开朗了,这就解释了为什么使用大于等0来判断线程状态,为了就是刚创建完成的worker线程是不需要被中断的。

tryTerminate

我们会发现在shutdown和shutdownNow方法的最后都调用了tryTerminate方法,那么其中都做了什么操作?

final void tryTerminate() {
       for (;;) {
           // 获取线程池状态
           int c = ctl.get();
           
           // 检查是否可以Terminate
           if (
        // 状态处于Running
               isRunning(c) ||
               // 大于等于TIDYING=TIDYING、TERMINATED
               runStateAtLeast(c, TIDYING) ||
               // 处于SHUTDOWN状态且任务队列非空
              (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
               return;
          // ①执行到此说明状态处于SHUTDOWN且任务队列为空,说明可以开始终结线程池了
           if (workerCountOf(c) != 0) {
               // 如果当前工作线程不为0,则中断1个工作线程,具体为什么是一个,下文会有解释
               interruptIdleWorkers(ONLY_ONE);
               return;
          }

           // 执行至此,说明工作线程数已为0,且状态处于SHUTDOWN,任务队列为空
           final ReentrantLock mainLock = this.mainLock;
           mainLock.lock();
           try {
               // 尝试CAS修改状态为TIDYING
               if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                   try {
                       // 钩子方法,子类实现
                       terminated();
                  } finally {
                       // 钩子方法执行成功,设置状态为TERMINATED
                       ctl.set(ctlOf(TERMINATED, 0));
                       // 唤醒所有等待TERMINATED的线程,下文有会有解释
                       termination.signalAll();
                  }
                   return;
              }
          } finally {
               mainLock.unlock();
          }
           // else retry on failed CAS
      }
  }

阅读完该方法的小伙伴肯定不理解,①处代码为什么判断工作线程不等于0,只中断一个工作线程,而不是中断所有工作线程。我们先上结论,不中断所有线程的原因是为了,到时候还需要至少一个线程执行①后面的代码,也就是需要线程去修改线程状态,不然就没有线程就修改状态了。

听到这个结论,小伙伴肯定是一头雾水,当初我看到这段源码也是一头雾水,不知道老爷子为什么这么设计。直到我在IDE里点击该方法查看它的引用时,一切就豁然开朗了。

如上图我们会发现,tryTerminate()方法,在processWorkerExit()方法中被引用到了,我们在 上一篇分析runWorker方法中会发现该方法处于,工作线程run的finally块中,也就是所有工作线程执行完成就会调用该方法。

到这里我们知道了工作线程的最后都将执行tryTerminate方法,但是还不能解释为什么工作线程数为0,而不是1呢,不是说保证至少一个工作线程存活执行就可以了吗?

我们的条件是工作线程数等于0,那么肯定有什么地方进行数量减少,而且这个方法要处于runWorker方法里面。顺藤摸瓜,跟着这个思路我们找到了getTask()方法,我们会发现①处,当处于SHUTDOWN时工作队列为空或状态为STOP、TIDYING、TERMINATED其中之一,就将工作线程数减一。其次我们会发现当工作线程处于②处等待获取task 时被中断的中断异常将会被③处抓住,然后重新回到①处减少工作线程数,也就是说最后肯定会存在最后一个线程将此处工作线程数减为0,然后就去修改线程池状态到终态。

一图胜千言,下面我通过流程图向大家展示内部是如何运转的。

我们将所有流程串联起来,左右两边是同时在运行的,所以这也就解释了为什么每次中断一个且工作线程等于0才会继续往下执行了。

等待线程池结束

经过上面长篇大论的论述,我们终于来到了最后一个方法中,最后一个方法也是一个非常简单的方法,我相信小伙伴们有了上面的基础,再来看这一段代码,简直是小巫见大巫。

public boolean awaitTermination(long timeout, TimeUnit unit)
       throws InterruptedException {
       long nanos = unit.toNanos(timeout);
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           for (;;) {
               // 如果当前状态为TERMINATED,就返回true
               if (runStateAtLeast(ctl.get(), TERMINATED))
                   return true;
               // 已超时,返回false
               if (nanos <= 0)
                   return false;
               // 未超时,继续等待
               nanos = termination.awaitNanos(nanos);
          }
      } finally {
           mainLock.unlock();
      }
  }

状态为TERMINATED直接返回、超时直接返回这两种情况都很好理解,那假如我在等待期间线程状态变为TERMINATED了呢。是不是需要某个人来将我从等待中唤醒。

我们发现在tryTerminate方法设置TERMINATED成功以后,马上就通知了所有在等待结束的线程了,那么这就串上了。

写在最后

看到这里的小伙伴,小张非常感谢你,谢谢你能够坚持看到这里。其次你也得感谢自己,感谢自己的坚持看到这里,将一个线程池核心源码盘然巨物给阅读完了。相信在这个过程,你也学习到了不少知识。在实际业务中,你可能不会写出那么复杂的代码,各种状态充斥在代码中,但是老爷子其中的不少思想都是我们可以借鉴学习。比如我们在第一篇中写到抽象模板方法、第二篇中CAS无锁编程、第三篇如何中断执行中的线程等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值