线程池 #
基础知识 #
- InterruptedException是checked异常,必须处理。
- 线程池的工作线程会捕获InterruptedException,并根据线程池状态决定是否继续获取任务或终止。并且其会将线程中断状态清除,因此如果需要后续检测中断状态,需要手动重新设置中断标志。
catch(InterruptedException e) {
// 检测到中断,线程池可以选择退出或继续
Thread.currentThread().interrupt(); // 重新设置中断标志
}
如果在一个函数中已经捕获(catch)了 InterruptedException,那么该异常不会再向上传递,上层函数就不需要再 try-catch 这个异常了。
但需要注意两点:
- 如果 catch 后不做任何处理,线程的中断标志会被清除(即
Thread.interrupted()变为 false),上层检测不到中断状态。 - 如果 catch 后希望上层还能检测到中断状态,应在 catch 块中调用
Thread.currentThread().interrupt();重新设置中断标志。
常见写法如下:
try {
// 可能抛出InterruptedException的方法
Thread.sleep(1000);
} catch (InterruptedException e) {
// 重新设置中断标志,便于上层检测
Thread.currentThread().interrupt();
}
这样,上层函数可以通过 Thread.currentThread().isInterrupted() 检测到中断状态,无需再 try-catch InterruptedException。
在 catch 块中先恢复中断标志,然后继续将异常抛出,这样既不会丢失中断信号,也能让上层调用者感知并决定如何处理。
推荐写法如下:
try {
// 可能抛出InterruptedException的方法
Thread.sleep(1000);
} catch (InterruptedException e) {
// 重新设置中断标志,便于上层检测
Thread.currentThread().interrupt();
throw e; // 继续向上抛出
}
这样既保证了中断信号不丢失,也让上层有机会处理异常。
线程池队列 #
Java 线程池(如 ThreadPoolExecutor)支持多种类型的任务队列(BlockingQueue),不同队列类型会影响线程池的行为和任务调度方式。常见的队列有以下几种:
1. SynchronousQueue #
- 特点:不存储任何元素,每个插入操作必须等待另一个线程来移除元素,反之亦然。
- 容量:容量为 0(不存储任务)。
- 适用场景:任务直接交给线程处理,如果没有空闲线程则尝试创建新线程,直到达到最大线程数。适合任务处理速度快、线程数弹性需求大的场景。
- 线程池行为:核心线程数为 0 时,适用于
CachedThreadPool,线程数可无限扩展(受最大线程数限制)。
2. LinkedBlockingQueue #
- 特点:基于链表的有界/无界阻塞队列,FIFO 顺序。
- 容量:默认 Integer.MAX_VALUE(几乎等于无界),也可指定容量。
- 适用场景:适合任务量大但线程数有限的场景,任务会被缓存在队列中等待线程处理。
- 线程池行为:适用于
FixedThreadPool,线程数固定,队列可缓存大量任务,线程数不会超过核心线程数。
3. ArrayBlockingQueue #
- 特点:基于数组的有界阻塞队列,FIFO 顺序。
- 容量:必须在构造时指定容量,容量固定。
- 适用场景:适合需要严格限制队列长度的场景,防止任务堆积过多导致内存溢出。
- 线程池行为:线程数达到核心线程数后,任务进入队列,队列满后才会创建新线程(不超过最大线程数)。
4. PriorityBlockingQueue #
- 特点:支持优先级排序的无界阻塞队列,元素必须实现
Comparable接口或提供Comparator。 - 容量:无界(Integer.MAX_VALUE)。
- 适用场景:适合需要按优先级处理任务的场景,如定时任务、调度系统等。
- 线程池行为:线程数达到核心线程数后,任务进入队列,线程数不会超过核心线程数。
5. DelayedWorkQueue(ScheduledThreadPoolExecutor 专用) #
- 特点:支持延迟和定时任务的无界阻塞队列,元素需实现
Delayed接口。 - 容量:无界。
- 适用场景:适合定时任务、周期性任务调度。
- 线程池行为:只用于
ScheduledThreadPoolExecutor,不直接用于普通线程池。
队列类型与线程池线程数量关系 #
| 队列类型 | 线程池线程数增长方式 | 典型线程池 |
|---|---|---|
| SynchronousQueue | 任务无法排队,直接创建新线程 | CachedThreadPool |
| LinkedBlockingQueue | 线程数达到核心数后任务排队,不扩容 | FixedThreadPool |
| ArrayBlockingQueue | 线程数达到核心数后任务排队,队列满后扩容 | 自定义线程池 |
| PriorityBlockingQueue | 线程数达到核心数后任务排队,不扩容 | 自定义线程池 |
- SynchronousQueue:最大线程数决定最大并发任务数。
- LinkedBlockingQueue/PriorityBlockingQueue:核心线程数决定最大并发任务数,队列可缓存大量任务。
- ArrayBlockingQueue:核心线程数 + 队列容量 + 最大线程数共同决定最大承载能力。
总结:
选择合适的队列类型和容量,是设计高效、稳定线程池的关键。实际应用中应根据业务特点、任务量和资源限制合理
线程池停机 #
shutdown 和 shutdownNow 的区别与原理 #
在 Java 的线程池(如 ThreadPoolExecutor)中,关闭线程池主要有两种方法:shutdown() 和 shutdownNow()。
1. shutdown() #
- 作用:启动线程池的有序关闭过程。
- 行为:
- 不再接受新的任务提交。
- 已经提交(包括正在执行和队列中等待的)任务会继续执行,直到全部完成。
- 线程池中的线程在任务执行完毕后会逐步退出。
- 常用场景:希望线程池优雅关闭,不丢失任何已提交的任务。
2. shutdownNow() #
- 作用:尝试立即关闭线程池。
- 行为:
- 不再接受新的任务提交。
- 会尝试中断所有正在执行的任务(通过调用线程的
interrupt()方法)。 - 返回尚未开始执行的任务列表(即队列中还未被取出的任务)。
- 不能保证所有正在执行的任务都能被成功中断,具体取决于任务代码是否响应中断。
- 返回值是一个 List,包含所有未开始执行的任务。类型是
List<Runnable>。
- 常用场景:需要尽快停止线程池,且可以接受部分任务未完成或被中断。
shutdownNow() 方法会对线程池中所有正在运行的工作线程调用 interrupt() 方法,尝试中断这些线程。具体来说:
shutdownNow()首先会设置线程池的状态为 STOP,不再接受新任务。- 然后会遍历线程池中的所有工作线程,依次调用每个线程的
interrupt()方法,向线程发送中断信号。 - 如果线程正在执行会响应中断的阻塞方法(如
sleep()、wait()、BlockingQueue.take()等),就会抛出InterruptedException,线程有机会及时退出。 - 如果线程没有响应中断(比如在执行普通计算),则需要任务代码自行检测中断状态。
注意:interrupt() 只是设置线程的中断标志位,不会强制终止线程,线程必须自己配合检测和响应中断。
3. 代码示例 #
ExecutorService executor = Executors.newFixedThreadPool(2);
// 优雅关闭
executor.shutdown();
// 立即关闭
List<Runnable> notStartedTasks = executor.shutdownNow();
4. 状态变化 #
- 调用
shutdown()或shutdownNow()后,可以通过isShutdown()判断线程池是否处于关闭状态。 - 通过
isTerminated()判断线程池是否彻底终止(所有任务都已完成,所有线程都已退出)。
5. 注意事项 #
shutdown()和shutdownNow()都不会阻塞主线程。如果需要等待线程池完全关闭,可以调用awaitTermination()方法。shutdownNow()并不能保证所有任务都被中断,只有那些能响应中断的任务才会被终止。
shutdown 和 shutdownNow 的真实案例 #
案例一:使用 shutdown() 优雅关闭线程池 #
场景:批量处理数据,所有任务都需要执行完毕后再关闭线程池。
import java.util.concurrent.*;
public class ShutdownExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交多个任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 开始执行");
try {
Thread.sleep(2000); // 模拟任务耗时
} catch (InterruptedException e) {
System.out.println("任务 " + taskId + " 被中断");
}
System.out.println("任务 " + taskId + " 执行完毕");
});
}
// 优雅关闭线程池
executor.shutdown();
System.out.println("已调用 shutdown(),不再接受新任务");
// 等待线程池完全关闭
if (executor.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("所有任务执行完毕,线程池关闭");
} else {
System.out.println("超时,仍有任务未完成");
}
}
}
输出示例:
任务 0 开始执行
任务 1 开始执行
已调用 shutdown(),不再接受新任务
任务 0 执行完毕
任务 2 开始执行
任务 1 执行完毕
任务 3 开始执行
任务 2 执行完毕
任务 4 开始执行
任务 3 执行完毕
任务 4 执行完毕
所有任务执行完毕,线程池关闭
说明:
shutdown() 后,线程池不再接受新任务,但会等待所有已提交任务执行完毕,保证任务不丢失,实现优雅关闭。
案例二:使用 shutdownNow() 立即关闭线程池 #
场景:系统检测到紧急情况,需要尽快终止所有任务。
import java.util.concurrent.*;
public class ShutdownNowExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交多个任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 开始执行");
try {
Thread.sleep(5000); // 模拟长时间任务
} catch (InterruptedException e) {
System.out.println("任务 " + taskId + " 被中断");
return;
}
System.out.println("任务 " + taskId + " 执行完毕");
});
}
// 等待一会儿后立即关闭线程池
Thread.sleep(2000);
System.out.println("调用 shutdownNow(),尝试立即终止所有任务");
java.util.List<Runnable> notStartedTasks = executor.shutdownNow();
System.out.println("未开始执行的任务数:" + notStartedTasks.size());
// 等待线程池关闭
if (executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("线程池已关闭");
} else {
System.out.println("线程池关闭超时");
}
}
}
输出示例:
任务 0 开始执行
任务 1 开始执行
调用 shutdownNow(),尝试立即终止所有任务
任务 0 被中断
任务 1 被中断
未开始执行的任务数:3
线程池已关闭
说明:
shutdownNow() 会尝试中断正在执行的任务(通过 interrupt),并返回还未开始执行的任务列表。只有那些能响应中断的任务才会被及时终止,部分任务可能未被执行。
总结:
shutdown()适用于需要保证所有任务都能执行完毕的场景,关闭过程更平滑。shutdownNow()适用于紧急停止线程池的场景,可能导致部分任务未完成或被中断,需任务代码能正确响应中断
抛异常InterruptedException #
在 Java 线程池中,线程在执行任务时,只有在调用会响应中断的阻塞方法时才会抛出 InterruptedException。常见的情况包括:
- 调用 Object 的 wait() 方法时被中断
- 调用 Thread 的 sleep() 方法时被中断
- 调用某些阻塞队列的方法(如 BlockingQueue 的 take、put、poll 等)时被中断
- 调用 CountDownLatch、CyclicBarrier、Semaphore 等同步工具的等待方法时被中断
- 调用 Future 的 get()、awaitTermination() 等等待方法时被中断
注意:
如果任务代码中没有调用这些会抛出 InterruptedException 的方法,即使线程被中断,也不会抛出该异常。
简而言之,只有在任务代码或线程池内部代码中,线程正处于阻塞等待状态,并且调用了会响应中断的方法时,被中断才会抛出 InterruptedException。
检测InterruptedException #
Java 线程池(如 ThreadPoolExecutor)在执行任务时,会通过工作线程捕获和处理 InterruptedException。下面详细介绍其检测和处理流程:
1. 线程池中的任务执行 #
线程池中的每个工作线程通常会循环从任务队列中获取任务并执行,伪代码如下:
while (true) {
Runnable task = getTask();
if (task == null) break;
try {
task.run();
} catch (Exception e) {
// 处理异常
}
}
2. InterruptedException 的检测 #
- 当线程池调用
getTask()(通常是阻塞队列的take()或poll()方法)时,如果线程被中断,会抛出InterruptedException。 - 线程池内部会捕获这个异常,并根据线程池的状态决定是否继续获取任务或终止线程。
示例(ThreadPoolExecutor 部分源码简化):
try {
Runnable r = workQueue.take(); // 可能抛出 InterruptedException
r.run();
} catch (InterruptedException e) {
// 检测到中断,线程池可以选择退出或继续
}
3. 线程池如何处理 InterruptedException #
- 如果线程池正在关闭(shutdown),检测到中断后,线程会退出循环,最终终止。
- 如果线程池未关闭,通常会忽略中断并继续尝试获取任务,保证线程池的稳定运行。
4. 任务中的 InterruptedException #
- 如果提交到线程池的任务内部主动检测中断(如在任务代码中调用阻塞方法),也可能抛出 InterruptedException。
- 线程池不会自动处理任务内部的中断,需任务代码自行捕获和响应。
总结 #
Java 线程池通过捕获工作线程在阻塞等待任务时抛出的 InterruptedException 来检测线程是否被中断,并根据线程池的状态决定线程的生命周期。任务内部的中断需开发者自行处理。
如需源码细节,可参考 ThreadPoolExecutor 的 runWorker 方法实现。
线程在for循环计算不会自动响应中断 #
如果线程执行的任务只是长时间的 for 循环计算,没有调用任何会抛出 InterruptedException 的阻塞方法(如 sleep、wait、IO 等),那么即使线程池调用了 shutdownNow(),线程只是被设置了中断标志位,但不会自动停止,也不会抛出异常。
只有当任务代码主动检测中断状态(如在循环中定期检查 Thread.currentThread().isInterrupted()),并在检测到中断时主动退出,线程才会及时响应中断。
示例:
while (true) {
// 计算逻辑
if (Thread.currentThread().isInterrupted()) {
// 响应中断,安全退出
break;
}
}
总结:
- 纯计算任务不会自动响应中断,必须在代码中主动检测并处理中断标志,否则线程会一直运行下去。
shutdownNow()只是设置中断标志,不会强制终止线程。
Java 的线程中断机制详解 #
Java 的线程中断是一种协作式的线程终止机制,允许一个线程通过“中断”信号请求另一个线程停止当前操作。中断本身不会强制终止线程,而是由被中断线程自行检测和响应。
1. 中断的基本方法 #
Thread.interrupt():向目标线程发送中断信号,将线程的中断标志位设为 true。Thread.isInterrupted():判断线程的中断标志位是否被设置(不会清除标志位)。Thread.interrupted():判断当前线程的中断标志位是否被设置,并将其清除(静态方法)。
2. 中断的实现原理 #
每个线程对象都有一个“中断标志位”。调用 interrupt() 方法时,这个位被设置为 true。线程本身不会因为标志位被设置就自动停止,需要线程自己在合适的位置检测并响应中断。
3. 检测和响应中断 #
- 主动检测:线程可以在执行过程中周期性地调用
isInterrupted()或interrupted()检查中断状态,并决定是否提前结束任务。 - 阻塞方法响应:如果线程正在调用如
Object.wait()、Thread.sleep()、BlockingQueue.take()等会抛出InterruptedException的阻塞方法,被中断时会抛出该异常,线程可以在 catch 块中进行清理和退出。
4. 示例代码 #
public class InterruptDemo implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟阻塞操作
Thread.sleep(1000);
} catch (InterruptedException e) {
// 捕获到中断异常,进行善后处理
System.out.println("线程被中断,准备退出");
Thread.currentThread().interrupt(); // 重新设置中断标志
break;
}
// 其他业务逻辑
}
System.out.println("线程安全退出");
}
}
5. 注意事项 #
- 中断不是强制终止:线程必须自己配合检测和响应,才能安全退出。
- 阻塞方法会清除中断标志:如
sleep()、wait()抛出InterruptedException时会自动清除中断标志,若需要后续检测,需手动重新设置。 - 线程池中的线程:线程池会复用线程,任务内部应妥善处理中断,避免影响后续任务。
6. 典型应用场景 #
- 线程池关闭时中断工作线程
- 取消耗时任务
- 响应外部停止信号
总结:
Java 的线程中断机制是一种安全、灵活的线程协作终止方式,推荐在多线程编程中优先采用。
Java 底层的中断处理机制原理 #
Java 的线程中断机制底层依赖于每个线程对象内部维护的一个“中断标志位”(interrupted flag)。这个机制主要由 JVM 和操作系统线程调度配合实现,具体原理如下:
1. 中断标志位 #
- 每个 Java 线程(Thread 实例)在 JVM 层面都有一个 boolean 类型的中断标志位,初始为
false。 - 当调用
thread.interrupt()方法时,JVM 会将该线程对象的中断标志位置为true,不会直接停止线程。
2. 检查与响应 #
- 线程自身可以通过
isInterrupted()或interrupted()方法检查这个标志位。 - 某些阻塞方法(如
Object.wait()、Thread.sleep()、BlockingQueue.take()等)在检测到线程被中断时,会抛出InterruptedException,并自动清除中断标志位(即重置为false)。
3. 阻塞方法的实现 #
- 阻塞方法内部会周期性检查线程的中断标志位。
- 如果检测到中断,方法会提前返回并抛出
InterruptedException,让线程有机会进行清理和退出。
4. 操作系统配合 #
- Java 线程通常映射为操作系统的原生线程(如 Windows 的 native thread 或 Linux 的 pthread)。
- 但 Java 的中断机制不会直接调用操作系统的线程终止或中断 API,而是完全由 JVM 维护标志位和调度逻辑,保证跨平台一致性。
5. 线程池与中断 #
- 线程池调用
shutdownNow()时,会对所有工作线程调用interrupt(),设置中断标志位。 - 线程池中的线程如果正处于阻塞状态(如等待任务),会因中断抛出异常并响应停机。
6. 总结 #
- 中断是一种协作机制,线程本身不会被强制终止,必须在合适位置主动检测和响应中断。
- JVM 通过维护线程的中断标志位和在阻塞方法中抛出异常,实现了安全、可控的线程中断机制。
参考:
CountDownLatch 详解 #
1. 基本概念 #
CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个同步辅助类,用于让一个或多个线程等待,直到其他线程完成一组操作。它常用于“主线程等待子线程任务完成”或“多线程并发起步”场景。
- 构造方法:
CountDownLatch(int count),参数 count 表示计数器的初始值。 - 核心方法:
await():调用线程等待,直到计数器为 0。countDown():计数器减 1,当计数器减到 0 时,所有等待的线程被唤醒。
2. await 的用法 #
await() 方法会让当前线程进入等待状态,直到:
- 计数器变为 0(即所有需要完成的事件都已完成),或
- 当前线程被中断(此时会抛出
InterruptedException)。
常见用法示例:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务...");
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
System.out.println(Thread.currentThread().getName() + " 任务完成");
latch.countDown(); // 计数器减 1
}).start();
}
System.out.println("主线程等待所有子线程完成...");
latch.await(); // 主线程阻塞,直到计数器为 0
System.out.println("所有子线程已完成,主线程继续执行");
}
}
输出示例:
Thread-0 正在执行任务...
Thread-1 正在执行任务...
Thread-2 正在执行任务...
主线程等待所有子线程完成...
Thread-0 任务完成
Thread-1 任务完成
Thread-2 任务完成
所有子线程已完成,主线程继续执行
3. InterruptedException 出现的位置 #
await()方法是一个可中断的阻塞方法,如果当前线程在等待过程中被中断,会立即抛出InterruptedException,并清除线程的中断标志。- 这意味着你需要在调用
await()的地方用 try-catch 捕获InterruptedException,或者继续向上抛出。
示例:
try {
latch.await();
} catch (InterruptedException e) {
// 处理中断逻辑,比如恢复中断标志或退出
Thread.currentThread().interrupt();
System.out.println("主线程等待时被中断");
}
4. 总结 #
CountDownLatch用于线程间的等待与通知,常用于主线程等待多个子任务完成。await()方法会阻塞当前线程,直到计数器为 0 或线程被中断。- 如果线程在
await()阻塞期间被中断,会抛出InterruptedException,需要正确处理。
参考:
awaitTermination 详解 #
awaitTermination 是 Java 线程池(如 ExecutorService)提供的一个方法,用于阻塞当前线程,等待线程池关闭。
方法签名 #
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
作用 #
- 当你调用
shutdown()或shutdownNow()后,线程池会进入关闭状态,但此时线程池中的任务可能还没有全部执行完毕。 awaitTermination方法会让当前线程(通常是主线程)阻塞,直到以下三种情况之一发生:- 线程池中的所有任务都执行完毕,线程池彻底关闭(此时返回
true)。 - 等待超时时间到达(此时返回
false)。 - 当前线程在等待期间被中断(此时抛出
InterruptedException)。
- 线程池中的所有任务都执行完毕,线程池彻底关闭(此时返回
常见用法 #
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务...
executor.shutdown(); // 启动优雅关闭
try {
// 最多等待10秒,直到线程池关闭
boolean terminated = executor.awaitTermination(10, TimeUnit.SECONDS);
if (terminated) {
System.out.println("线程池已关闭,所有任务执行完毕");
} else {
System.out.println("超时,线程池仍未完全关闭");
}
} catch (InterruptedException e) {
System.out.println("等待线程池关闭时被中断");
Thread.currentThread().interrupt();
}
注意事项 #
- 必须先调用
shutdown()或shutdownNow(),否则awaitTermination会一直等待。 - 如果线程池在指定时间内没有关闭,方法会返回
false,你可以选择后续处理(如强制关闭等)。 - 如果当前线程在等待期间被中断,会抛出
InterruptedException,需要捕获并处理。
典型场景 #
- 主线程需要等待线程池中所有任务执行完毕后再继续后续操作(如资源释放、程序退出等)。
- 结合
shutdown()和awaitTermination()实现线程池的优雅关闭。
总结 #
awaitTermination用于等待线程池关闭,保证所有任务执行完毕或超时后再继续执行主流程。- 是多线程编程中线程池资源管理的重要
如何停止一个 Java 应用 #
Java 应用的停止方式主要有以下几种,常见场景和推荐做法如下:
1. 正常停止(优雅关闭) #
- 主线程执行完毕:主方法(
main)执行结束,且没有非守护线程在运行,JVM 会自动退出。 - 关闭线程池:如果应用中有线程池,建议先调用
shutdown(),再用awaitTermination()等待所有任务完成,最后主线程退出。 - 关闭资源:在退出前关闭数据库连接、文件流、网络连接等资源,避免资源泄漏。
- 使用钩子线程:可以通过
Runtime.getRuntime().addShutdownHook(Thread hook)注册关闭钩子,在 JVM 退出前执行清理逻辑。
示例:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("应用即将关闭,执行清理工作...");
// 关闭资源、保存状态等
}));
2. 强制停止 #
- System.exit(int status):立即终止 JVM,status=0 表示正常退出,非0表示异常退出。会执行所有已注册的关闭钩子,但不会等待线程池任务完成。
- kill 进程:在操作系统层面直接杀死 Java 进程(如 Linux 下
kill命令),此方式不推荐,可能导致资源未释放、数据丢失。
示例:
System.exit(0); // 强制退出
3. 响应外部信号 #
- 接收 SIGTERM/SIGINT 信号:如在容器、服务环境下,收到 kill、Ctrl+C 等信号时,JVM 会自动触发关闭钩子,应用可在钩子中优雅停机。
4. 停止线程的正确方式 #
- 不要使用 Thread.stop()(已废弃,危险)。
- 推荐通过设置中断标志(
interrupt()),让线程在合适位置检测并响应中断,安全退出。
示例:
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
}
总结 #
- 推荐优雅关闭:先关闭线程池和资源,再让主线程退出。
- 可用关闭钩子做最后的清理。
- 避免强制杀进程或使用已废弃的 Thread.stop()。
- 多线程应用要确保所有线程都能正确响应中断和