在日常开发中为了便于线程的有效复用,经常会用到线程池,然而使用完线程池后如果不调用 shutdown 关闭线程池,则会导致线程池资源一直不被释放。下面通过简单的例子来说明该问题。

问题复现

下面通过一个例子说明如果不调用线程池对象的 shutdown 方法关闭线程池,则当线程池里面的任务执行完毕并且主线程已经退出后,JVM 仍然存在。

  1. public class TestShutDown {
  2. static void asynExecuteOne() {
  3. ExecutorService executor = Executors.newSingleThreadExecutor();
  4. executor.execute(new Runnable() {
  5. public void run() {
  6. System.out.println("--async execute one ---");
  7. }
  8. });
  9. }
  10. static void asynExecuteTwo() {
  11. ExecutorService executor = Executors.newSingleThreadExecutor();
  12. executor.execute(new Runnable() {
  13. public void run() {
  14. System.out.println(「--async execute two ---」);
  15. }
  16. });
  17. }
  18. public static void mainString[] args {
  19. //(1)同步执行
  20. System.out.println(「---sync execute---」);
  21. //(2)异步执行操作 one
  22. asynExecuteOne();
  23. //(3)异步执行操作 two
  24. asynExecuteTwo();
  25. //(4)执行完毕
  26. System.out.println(「---execute over---」);
  27. }
  28. }

在如上代码的主线程里面,首先同步执行了代码(1),然后执行代码(2)和代码(3),代码(2)和代码(3)使用线程池的一个线程执行异步操作,我们期望当主线程与代码(2)和代码(3)执行完线程池里面的任务后整个 JVM 就会退出,但是执行结果却如下所示。

使用线程池的情况下当程序结束时记得调用 shutdown 关闭线程池 - 图1

右上的方块说明 JVM 进程还没有退出,在 Mac 上执行 ps -eaf|grep java 命令后发现 Java 进程还存在,这是什么情况呢?修改代码(2)和代码(3),在方法里面添加调用线程池的 shutdown 方法的代码。

  1. static void asynExecuteOne() {
  2. ExecutorService executor = Executors.newSingleThreadExecutor();
  3. executor.execute(new Runnable() {
  4. public void run() {
  5. System.out.println("--async execute one ---");
  6. }
  7. });
  8. executor.shutdown();
  9. }
  10. static void asynExecuteTwo() {
  11. ExecutorService executor = Executors.newSingleThreadExecutor();
  12. executor.execute(new Runnable() {
  13. public void run() {
  14. System.out.println("--async execute two ---");
  15. }
  16. });
  17. executor.shutdown();
  18. }

再次执行代码你会发现 JVM 已经退出了,使用 ps -eaf|grep java 命令查看,发现 Java 进程已经不存在了,这说明只有调用了线程池的 shutdown 方法后,线程池任务执行完毕,线程池资源才会被释放。

问题分析

下面看为何会如此?大家或许还记得在基础篇讲解的守护线程与用户线程,JVM 退出的条件是当前不存在用户线程,而线程池默认的 ThreadFactory 创建的线程是用户线程。

  1. static class DefaultThreadFactory implements ThreadFactory {
  2. ...
  3. public Thread newThread(Runnable r) {
  4. Thread t = new Thread(group, r,
  5. namePrefix + threadNumber.getAndIncrement(),
  6. 0);
  7. if (t.isDaemon())
  8. t.setDaemon(false);
  9. if (t.getPriority() ! = Thread.NORM_PRIORITY)
  10. t.setPriority(Thread.NORM_PRIORITY);
  11. return t;
  12. }
  13. }

由如上代码可知,线程池默认的 ThreadFactory 创建的都是用户线程。而线程池里面的核心线程是一直存在的,如果没有任务则会被阻塞,所以线程池里面的用户线程一直存在。而 shutdown 方法的作用就是让这些核心线程终止,下面简单看下 shutdown 的主要代码。

  1. public void shutdown() {
  2. final ReentrantLock mainLock = this.mainLock
  3. mainLock.lock();
  4. try {
  5. ...
  6. //设置线程池状态为 SHUTDOWN
  7. advanceRunStateSHUTDOWN);
  8. //中断所有的空闲工作线程
  9. interruptIdleWorkers();
  10. ...
  11. } finally {
  12. mainLock.unlock();
  13. }
  14. ...
  15. }

这里在 shutdown 方法里面设置了线程池的状态为 SHUTDOWN,并且设置了所有 Worker 空闲线程(阻塞到队列的 take()方法的线程)的中断标志。那么下面来看在工作线程 Worker 里面是不是设置了中断标志,然后它就会退出。

  1. final void runWorker(Worker w) {
  2. ...
  3. try {
  4. while (task ! = null || (task = getTask()) ! = null) {
  5. ...
  6. }
  7. ...
  8. } finally {
  9. ...
  10. }
  11. }
  12. private Runnable getTask() {
  13. boolean timedOut = false;
  14. for (; ; ) {
  15. ...
  16. //(1)
  17. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
  18. decrementWorkerCount();
  19. return null;
  20. }
  21. try {
  22. //(2)
  23. Runnable r = timed ?
  24. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  25. workQueue.take();
  26. if (r ! = null)
  27. return r;
  28. timedOut = true;
  29. } catch (InterruptedException retry) {
  30. timedOut = false;
  31. }
  32. }
  33. }

在如上代码中,在正常情况下如果队列里面没有任务,则工作线程被阻塞到代码(2)等待从工作队列里面获取一个任务。这时候如果调用线程池的 shutdown 命令(shutdown 命令会中断所有工作线程),则代码(2)会抛出 InterruptedException 异常而返回,而这个异常被捕捉到了,所以继续执行代码(1),而执行 shutdown 时设置了线程池的状态为 SHUTDOWN,所以 getTask 方法返回了 null,因而 runWorker 方法退出循环,该工作线程就退出了。

小结

本节通过一个简单的使用线程池异步执行任务的案例介绍了使用完线程池后如果不调用 shutdown 方法,则会导致线程池的线程资源一直不会被释放,并通过源码分析了没有被释放的原因。所以在日常开发中使用线程池后一定不要忘记调用 shutdown 方法关闭。