拆解ThreadPoolExecutor之关闭线程池和钩子函数方法的各个细节

author:编程界的小学生

date:2021/05/31

flag:不写垃圾没有营养的文章!

如果下面的问题你都会的话就别在这浪费时间啦

  • 从源码层面分析shutdown()和shutdownNow()有什么区别?
  • shutdwon是怎么保证workers都执行完才进行中断线程的?
  • shutdownNow后会把当前正在执行的任务给执行完吗?
  • Worker为啥继承AQS?
  • shutdown后线程池里的线程怎么处理的?
  • 中断空闲线程?哪些是空闲线程?
  • 获取任务getTask,没有任务的时候一直阻塞,什么时候不再阻塞?
  • 如何优雅关闭线程池?shutdown+awaitTermination

零、初始shutdown和shutdownNow

先来看看这二者之间源码上到底有何差异?一图胜千言:
在这里插入图片描述

一、shutdown

1、全貌

public void shutdown() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    checkShutdownAccess();
    advanceRunState(SHUTDOWN);
    interruptIdleWorkers();
    onShutdown(); // hook for ScheduledThreadPoolExecutor
  } finally {
    mainLock.unlock();
  }
  tryTerminate();
}

2、为什么上锁?

废话嘛,防止多个线程同时shutdown。

3、checkShutdownAccess

检查jvm权限的,就当这个方法不存在。

4、advanceRunState

advanceRunState(SHUTDOWN);

private void advanceRunState(int targetState) {
  for (;;) {
    int c = ctl.get();
    if (runStateAtLeast(c, targetState) ||
        ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
      break;
  }
}

4.1、拆解1:runStateAtLeast

private static boolean runStateAtLeast(int c, int s) {
  return c >= s;
}

很简单,就是比大小。在这里就是我判断我线程池目前的状态是不是大于等于传递进来的线程池状态(SHUTDOWN),如果当前线程池状态比SHUTDOWN还大,那就直接是true,没必要进行后面的CAS了,直接break就完事了。

4.2、拆解2:compareAndSet

就是CAS设置值,这里就是CAS将ctl设置为ctlOf(targetState, workerCountOf(c)),因为ctl包含线程池状态和线程池中活跃线程数,所以ctlOf方法就是将targetState和workerCountOf©进行拼接成一个完整的ctl。

4.3、小结

就是通过CAS自旋的方式给线程池状态设置为SHUTDOWN。

5、interruptIdleWorkers

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();
  }
}

5.1、拆解1:参数onlyOne

true:只中断其中一个线程, false:for循环遍历全部线程,逐个中断,无一幸免。

可以看到if (onlyOne) break;,也就是说如果是true,第一轮for后就break了,只中断一个。

5.2、拆解2:为什么上锁

废话嘛,防止并发,有的人问了,shutdown那里不是已经上锁了吗,这里还有啥必要吗?

又不是只有shutdown在调此方法,还有其他地方的呀。

5.3、拆解3:for(Worker w : workers)

很简单,就是遍历当前线程池的全部任务,然后获取任务的线程,逐个进行interrupt中断。

5.4、拆解4:if判断

if (!t.isInterrupted() && w.tryLock()) {...}

很巧妙,tryLock承上启下,巧妙的一逼!

首先判断是不是被中断过了,如果已经被中断了,则下一轮循环,如果没被中断且w.tryLock()成功,则进行中断。

w.tryLock()是何意?下面分析也表明Worker为啥要继承AQS了。

public boolean tryLock()  { 
  return tryAcquire(1); 
}

protected boolean tryAcquire(int unused) {
  if (compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(Thread.currentThread());
    return true;
  }
  return false;
}

可以看到tryLock很简单,就干一件事,就是将state设置为1,如果设置失败,则return false,也就是不会走到if里面,不会进行中断线程。如果设置成功了则进行中断线程。这个tryLock是点睛之笔,用于判断此线程是不是空闲线程,如果是空闲线程则进行中断,因为线程池SHUTDOWN了嘛,空闲线程肯定要回收。

什么情况下tryLock会失败?

这要回溯到runWorker方法:

final void runWorker(Worker w) {
  try {
    while (task != null || (task = getTask()) != null) {
      w.lock();
    }
  } finally {
    w.unlock();
  }
}

看到了没,我先getTask()阻塞式获取任务,如果没获取到,肯定是阻塞了,如果获取到了,则给这个任务上锁,上锁解锁都干嘛了?

public void lock() {
  acquire(1); 
}

public void unlock() { 
  release(1); 
}

神他妈逻辑,就是给state+1,state-1的操作。大彻大悟,如果当前线程获取到了任务,则interruptIdleWorkers#tryLock肯定失败,因为获取到任务后已经抢占锁了,代表当前worker到线程是活跃线程,不是空闲线程,不可被中断。如果没获取任务,阻塞在getTask那里了,那肯定是没上锁的,那tryLock肯定会返回true,代表这个线程数空闲线程,可以被中断。中断线程后,此线程在getTask会立即不在阻塞,发生InterruptedException,也就是可以终止那些正在执行workQueue.take()方法的工作线程。

5.5、中断解锁

上面是本文重点之重,这就是Worker为啥继承AQS的关键原因,巧妙采取其state,来判断是否需要中断等骚操作。

最后就是中断和finally解锁啦。

解锁还有个点需要注意:就是需要解worker的锁

finally {
  w.unlock();
}

因为tryLock就是上锁的意思,只要返回true就代表上锁成功了,所以中断线程后别忘记解锁。

5.6、总结

这个方法就是:中断空闲的线程,很合理,因为都SHUTDOWN了,不接收新任务了,空闲的线程没啥用了。但是怎么确定当前线程是不是空闲线程的,这就很巧妙,巧妙的运用了AQS的state状态位。在runWorker里承上启下。只是中断了空闲线程,任务队列是饱满状态,线程忙不过来的话就不会中断任何线程, 会等执行完workers等任务。这就是SHUTDOWN。

6、onShutdown

钩子函数,默认是个空方法,模板方法,按需重写。就是在线程池shutdown后可以自定义一些操作。比如ScheduledThreadPoolExecutorThreadPoolExecutor的子类,他就重写了onShutdown方法来做一些自定义的事情。

7、tryTerminate

很重点的一个方法,下面单独分析。

8、小结

就是先CAS将线程池状态设置为SHUTDOWN,然后找到全部空闲线程进行中断,最后提供了个钩子方法onShutdown可以重写。都完事后调用tryTerminate。

二、shutdownNow

1、全貌

public List<Runnable> shutdownNow() {
  List<Runnable> tasks;
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    checkShutdownAccess();
    advanceRunState(STOP);
    interruptWorkers();
    tasks = drainQueue();
  } finally {
    mainLock.unlock();
  }
  tryTerminate();
  return tasks;
}

2、为什么上锁?

废话嘛,防止多个线程同时shutdownNow()。

3、checkShutdownAccess

检查jvm权限的,就当这个方法不存在。

4、advanceRunState

同上面shutdown,已经讲的很清晰了。只是这里设置的状态是STOP

5、interruptWorkers

private void interruptWorkers() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    // 循环全部任务,逐个中断。
    for (Worker w : workers)
      w.interruptIfStarted();
  } finally {
    mainLock.unlock();
  }
}

void interruptIfStarted() {
  Thread t;
  // 只要线程状态是大于等于0的(也就是说调用了线程的start方法,因为new Worker的时候state=-1),
  // 且线程没有被中断,那就中断,就是这么简单粗暴!
  if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    try {
      // 中断
      t.interrupt();
    } catch (SecurityException ignore) {
    }
  }
}

不再像shutdown那么友好,只中断空闲线程,shutdownNow的interruptWorkers会中断全部线程。大概原理就是:循环遍历线程池中的全部任务,如果这些任务的线程状态是大于等于0的(也就是说调用了线程的start方法,因为new Worker的时候state=-1),且线程没有被中断,那就中断,中断后getTask会抛出一个中断异常,顺带可以停止那些正在执行workQueue.take()方法的工作线程,就是这么简单粗暴!

6、drainQueue

private List<Runnable> drainQueue() {
  // 当前线程池的任务队列
  BlockingQueue<Runnable> q = workQueue;
  // 最终返回的结果
  ArrayList<Runnable> taskList = new ArrayList<Runnable>();
  // 将任务队列中的每个元素都放到ArrayList<Runnable>里面,每放成功一个就从q中移除一个
  q.drainTo(taskList);
  // 如果执行完drainTo后,q还不是空的,这是啥情况?
  // 1. 上面报错了,按道理来讲报错后就跳出方法了,所以不是此种可能。
  // 2. 为了延迟队列来的,延迟队列没有放进去ArrayList<Runnable>后将任务从老队列移除的操作,所以延迟队列的话就手动for
  if (!q.isEmpty()) {
    for (Runnable r : q.toArray(new Runnable[0])) {
      if (q.remove(r))
        taskList.add(r);
    }
  }
  // 返回最终的任务List
  return taskList;
}

看注释就行了,唯一需要注意两点:

  • 1.直接返回BlockingQueue<Runnable>不行嘛?为啥还要转成ArrayList返回?

    shutdownNow后肯定要清空任务队列的,shutdown不需要清空是因为他都会执行完。所以边清空边放到一个List里,统一返回类型。

  • 2.都drainTo了,为啥还要再次判空,手动remove/add?

    因为线程池的任务队列是用户自定义传参的,队列不同,drainTo的实现方案不同,如果是延迟队列的话是不具备删除功能的,所以手动remove/add。

7、tryTerminate

下面就开始讲这个!

8、返回

返回tasks!带返回值的!

9、小结

和shutdown很相似,区别在于:

  • shutdownNow没有onShutdown钩子函数,我个人认为是因为shutdownNow代表很紧急,我把未完成的任务都给你,紧急关闭就行了,不支持钩子。而shutdown比较优雅,不紧不慢的,支持钩子自定义一些东西。
  • shutdownNow中断全部线程,shutdown只中断空闲线程,忙着的线程会等处理完任务在中断。
  • shutdown没有返回值,shutdownNow会把当前任务队列里的任务转成ArrayList<Runnable>返回回去。
  • shutdown支持onShutdown钩子函数,shutdownNow不支持。
  • shutdown给线程池状态设置为SHUTDOWN,shutdownNow给线程池状态设置为STOP。

三、tryTerminate

1、全貌-带注释

final void tryTerminate() {
  //自旋
  for (;;) {
    //获取最新ctl值
    int c = ctl.get();
    //条件一:isRunning(c)  成立,直接返回就行,线程池很正常!
    //条件二:runStateAtLeast(c, TIDYING) 说明 已经有其它线程 在执行 TIDYING -> TERMINATED状态了,当前线程直接回去。
    //条件三:(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())
    //SHUTDOWN特殊情况,如果是这种情况,直接回去。得等队列中的任务处理完毕后,再转化状态。
    if (isRunning(c) ||
        runStateAtLeast(c, TIDYING) ||
        (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
      return;

    //条件成立:当前线程池中的线程数量 > 0
    if (workerCountOf(c) != 0) {
      // 中断一个空闲线程,注意是一个,不是全部,因为ONL_YONE是true,其实就是通过中断信号,唤醒阻塞的线程(getTask()阻塞的) 
      interruptIdleWorkers(ONLY_ONE);
      return;
    }

    final ReentrantLock mainLock = this.mainLock;
    //获取线程池全局锁
    mainLock.lock();
    try {
      //设置线程池状态为TIDYING状态。
      if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
        try {
          //调用钩子方法,默认空方法,需要自己重写。
          terminated();
        } finally {
          //设置线程池状态为TERMINATED状态。
          ctl.set(ctlOf(TERMINATED, 0));
          //唤醒调用 awaitTermination() 方法的线程。
          termination.signalAll();
        }
        return;
      }
    } finally {
      //释放线程池全局锁。
      mainLock.unlock();
    }
  }
}

2、分析

注释写的很详细了,着重说明一点:

//条件成立:当前线程池中的线程数量 > 0
if (workerCountOf(c) != 0) {
  // 中断一个空闲线程,注意是一个,不是全部,因为ONL_YONE是true,其实就是通过中断信号,唤醒阻塞的线程(getTask()阻塞的) 
  interruptIdleWorkers(ONLY_ONE);
  return;
}

为什么只中断其中一个空闲线程而不是全部呢?

原因就是因为tryTerminate方法不是只有在shutdown的时候才会调用,而是在运行完任务后processWorkerExit里面也会调用,所以每次执行完任务都会调用,所以每次都中断其中一个线程。

3、小结

这个方法就干了一件事:设置线程池状态为TERMINATED状态且唤醒调用 awaitTermination() 方法的线程。

四、awaitTermination

1、全貌

public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
  long nanos = unit.toNanos(timeout);
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    for (;;) {
      // 如果当前线程池状态大于等于TERMINATED了,也就是说已经被terminated了,则直接返回true
      if (runStateAtLeast(ctl.get(), TERMINATED))
        return true;
      // 如果达到超时时间,已经超时,则返回false
      if (nanos <= 0)
        return false;
      // 重置距离超时时间的剩余时长
      nanos = termination.awaitNanos(nanos);
    }
  } finally {
    mainLock.unlock();
  }
}

2、分析

private static boolean runStateAtLeast(int c, int s) {
  return c >= s;
}

在这里就是如果当前线程池状态大于等于TERMINATED了,也就是说已经被terminated了,则直接返回true。

如果达到超时时间,已超时,则返回false,否则就等待,重置距离超时时间的剩余时长。同时awaitNanos也会被tryTermination唤醒。

【微信公众号】
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【原】编程界的小学生

没有打赏我依然会坚持。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值