杰瑞科技汇

Java线程池shutdown的正确方法是什么?

shutdown() 方法的作用是优雅地关闭线程池,它不会立即销毁线程池和其中的线程,而是:

Java线程池shutdown的正确方法是什么?-图1
(图片来源网络,侵删)
  1. 停止接受新任务:调用 shutdown() 后,再向线程池提交新任务(通过 execute()submit())会被拒绝,并抛出 RejectedExecutionException
  2. 完成已提交任务:线程池会继续处理那些已经提交到任务队列(BlockingQueue)中的任务,直到所有任务都执行完毕。
  3. 终止空闲线程:当任务队列中的任务被处理完后,线程池会逐个终止(中断)那些空闲的(正在等待任务的)工作线程,直到所有线程都被销毁,线程池最终进入 TERMINATED 状态。

shutdown() 的详细行为与生命周期

要完全理解 shutdown(),我们需要了解线程池的整个生命周期,一个线程池的状态转换如下:

RUNNING -> SHUTDOWN -> STOP -> TIDYING -> TERMINATED
  • RUNNING: 线程池正在运行,可以接受新任务并处理队列中的任务。
  • SHUTDOWN: 调用 shutdown() 后进入此状态,不再接受新任务,但会处理完队列中的任务。
  • STOP: 调用 shutdownNow() 后进入此状态,不再接受新任务,并且会尝试中断正在执行的任务,同时清空任务队列。
  • TIDYING: 当所有任务都已终止,并且工作线程数量为 0 时,线程池会进入此状态,此时会调用 terminated() 钩子方法。
  • TERMINATED: terminated() 方法执行完毕后,线程池进入最终状态 TERMINATED

shutdown() 的具体步骤:

  1. 状态转换:将线程池的状态从 RUNNING 切换到 SHUTDOWN
  2. 中断空闲线程:调用 interruptIdleWorkers() 方法,该方法会遍历所有工作线程,尝试中断那些没有在执行任务的线程(即正在从队列中获取任务的线程),正在执行任务的线程不受影响。
  3. 任务处理:线程池会继续从任务队列中获取并执行任务,直到队列为空。
  4. 线程销毁:当队列为空后,工作线程在尝试从队列获取任务时,会发现自己被中断(因为 shutdown() 调用了 interruptIdleWorkers),于是它们会退出 run() 方法,线程被销毁。
  5. 最终状态:当最后一个工作线程被销毁后,线程池的状态最终会变为 TERMINATED

shutdown()shutdownNow() 的关键区别

这是一个非常常见的面试点。shutdownNow() 是“暴力”关闭,而 shutdown() 是“温柔”关闭。

特性 shutdown() shutdownNow()
新任务 拒绝,抛出 RejectedExecutionException 拒绝,抛出 RejectedExecutionException
队列中的任务 执行完毕 不执行,并尝试将它们从队列中移除返回
正在执行的任务 继续执行,不受影响 尝试中断(通过调用 Thread.interrupt()),但不保证一定能中断
状态转换 RUNNING -> SHUTDOWN -> TERMINATED RUNNING -> STOP -> TIDYING -> TERMINATED
返回值 void 返回一个 List<Runnable>,包含队列中尚未执行的任务

简单记忆

  • shutdown(): “等我忙完手头和队列里的活儿,我就下班了。”
  • shutdownNow(): “全体都有!立刻停止手头的工作,马上解散!队列里的活儿也别干了。”

如何确保线程池已完全关闭?

shutdown() 是一个非阻塞方法,调用它只是启动了关闭流程,如果你需要在关闭流程完成后执行某些操作(释放资源、记录日志),你需要等待线程池真正终止。

Java线程池shutdown的正确方法是什么?-图2
(图片来源网络,侵删)

Java 提供了两种方式来等待线程池终止:

awaitTermination(long timeout, TimeUnit unit)

这是最常用和推荐的方法,它会阻塞当前线程,直到线程池关闭完成,或者超时。

工作原理

  • 调用 shutdown() 后,紧接着调用 awaitTermination()
  • awaitTermination() 会定期(每 300ms 左右)检查线程池的状态是否为 TERMINATED
  • 如果是,则立即返回 true
  • 如果在指定的 timeout 时间内,线程池仍未终止,则方法返回 false
  • 如果等待过程中被中断,方法会抛出 InterruptedException

最佳实践代码示例

Java线程池shutdown的正确方法是什么?-图3
(图片来源网络,侵删)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ShutdownExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // 提交一些任务
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                try {
                    // 模拟任务执行
                    System.out.println(Thread.currentThread().getName() + " is running...");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " has finished.");
                } catch (InterruptedException e) {
                    // 线程被中断时打印信息
                    System.out.println(Thread.currentThread().getName() + " was interrupted.");
                }
            });
        }
        System.out.println("Shutting down the executor...");
        // 1. 启动优雅关闭
        executor.shutdown();
        try {
            // 2. 等待线程池终止,最多等待 5 秒
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                // 5 秒后仍未终止,可以采取进一步措施
                System.out.println("Executor did not terminate in 5 seconds. Forcing shutdown.");
                // executor.shutdownNow(); // 可以选择强制关闭
            }
        } catch (InterruptedException e) {
            // 如果等待被中断,则重新设置中断状态,并尝试强制关闭
            System.err.println("Await termination was interrupted.");
            executor.shutdownNow();
            // 保留中断状态
            Thread.currentThread().interrupt();
        }
        System.out.println("Executor has been shut down completely.");
    }
}

isTerminated()

这个方法的作用是检查线程池是否已经完全终止(即状态为 TERMINATED),它是一个非阻塞方法。

它会与一个循环和 awaitTermination 结合使用,或者用于在单独的线程中监控线程池状态。

// ... (接上面的代码)
executor.shutdown();
// 使用 isTerminated() 进行轮询检查(不推荐,会占用CPU)
while (!executor.isTerminated()) {
    // 等待一会儿再检查
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
System.out.println("Executor has been shut down completely (polled).");

为什么不推荐轮询 isTerminated() 因为它会不断地消耗 CPU 资源来检查状态,而 awaitTermination() 在等待期间是让出 CPU 的,效率更高,也更优雅。


完整的优雅关闭流程总结

在实际应用中,一个完整的、健壮的线程池关闭流程通常如下:

  1. 收到关闭信号:应用关闭钩子被触发,或某个服务被停止。
  2. 调用 shutdown():开始优雅关闭流程,拒绝新任务。
  3. 调用 awaitTermination(long timeout, TimeUnit unit):等待线程池在合理的时间内完成关闭。
  4. 处理超时awaitTermination 返回 false(表示超时),说明有任务可能执行时间过长或线程卡住了,此时可以:
    • 记录警告日志。
    • 调用 shutdownNow() 尝试强制关闭。
    • 或者根据业务需求决定是否等待更长时间。
  5. 处理中断awaitTermination 抛出 InterruptedException,说明等待线程被中断,此时应:
    • 恢复中断状态 (Thread.currentThread().interrupt())。
    • 调用 shutdownNow() 来确保线程池被关闭。
    • 处理中断逻辑。

遵循这个流程,可以确保你的应用程序在关闭时能够正确地释放资源,避免线程泄漏和任务丢失。

分享:
扫描分享到社交APP
上一篇
下一篇