1、为什么要优雅的退出线程?
如果直接强制退出线程,可能会造成以下问题:
程序未按指定的逻辑执行(比如,我想累加到 10,结果到 6 的时候你就给我关闭了)可能导致死锁(程序在未释放锁的情况下被强制停止)可能导致资源未及时释放(程序还未释放资源,就被强制停止了)不会抛出异常,导致无法正确处理异常信息
2、如何停止线程?
大致有以下几种方法:
stop():强制退出,不推荐。interrupt():设置中断标记,尝试停止线程。volatile 关键字:类似于设置中断标记。线程池的 shutdownNow():尝试停止执行中的线程,并返回未开始执行的任务列表。
2.1、stop()(不推荐)
示例代码
private static void testStop() throws Throwable {
Thread t = new Thread(() -> {
while(true) {
System.out.println("执行1");
int count = 0;
String a = "";
try {
for (int i = 0; i < 2000000000; i++) {
count += 1;
a = "" + i; // 减慢执行时间
}
System.out.println("执行2");
if(Thread.currentThread().isInterrupted()) {
System.out.println("线程设置了中断标记,停止, count = " + count);
break;
}
} catch (Exception e) {
throw new RuntimeException(); // 这里也可以选择抛出异常
} finally {
System.out.println("finally 执行3, count = " + count + "\n");
}
}
});
t.start();
Thread.sleep(10);
t.stop(); // 强制关闭线程
t.join(); // 等待线程执行完毕
}
输出:
我们的预期是,任务执行完循环之后再退出线程,但是这里还没有走完循环就直接强制退出了。
2.2、interrupt()
给线程设置一个中断标记,线程执行的时候判断是否设置了中断标记,如果设置了那么就尝试退出线程。
示例代码
private static void testInterrupt() throws Throwable{
Thread t = new Thread(() -> {
while(true) {
long count = 0;
try {
long pre = System.currentTimeMillis();
for (int i = 0; i < 2000000000; i++) {
count ++;
}
long after = System.currentTimeMillis();
System.out.println("任务所需时间: " + (after - pre)); // 计算上述循环所花时间,大致为 80ms
if(Thread.currentThread().isInterrupted()) { // 判断是否设置了中断标记,如果设置了就退出循环
System.out.println("需要中断, count = " + count);
break;
}
} catch (Exception e) {
break; // 抛出异常
} finally {
System.out.println("finally 执行3, count = " + count + "\n");
}
}
});
t.start();
long pre = System.currentTimeMillis();
Thread.sleep(10); // 等待 10 毫秒就停止线程,看看需要更多时间的任务是否需要停止
long after = System.currentTimeMillis();
System.out.println("线程停止前后时间: " + (after - pre)); // 大致为 23ms
t.interrupt(); // 设置中断标记
t.join(); // 等待线程执行完毕
}
输出:
可以使用 isInterrupted() 判断当前线程是否设置了中断标记。
isInterrupted() 的返回值:
true:设置了中断标记,并且清除中断标记。false:没有设置中断标记,或者是中断标记在之前就被清除了。
2.3、volatile 关键字
volatile 关键字简述:
可见性:保证线程对一个变量的更改时,其他线程能够立即看到该变量的最新值。(变量不走线程的缓存,而是直接从内存中读写)禁止指令重排序(本文不做详述)
如:
private static boolean shouldStop = false;
示例代码
private static void testVolatile() throws Throwable{
Thread t = new Thread(() -> {
while(!shouldStop) {
System.out.println("执行1");
try {
Thread.sleep(1000);
System.out.println("执行2");
} catch (InterruptedException e) {
throw new RuntimeException(); // 这里也可以选择抛出异常
} finally {
System.out.println("finally 执行3\n");
}
}
});
t.start();
Thread.sleep(1500); // 等待 1.5 秒
shouldStop = true;
t.join(); // 等待线程执行完毕
}
输出:
如果还不了解线程状态的读者,可以阅读这篇文章:java线程的生命周期
2.4、线程池的 shutdownNow()
shutdownNow():线程池状态变为 STOP,不仅拒绝新的任务,还会去尝试中断正在运行的线程(类似于 interrupt()),并且返回未开始执行的任务列表。
代码示例
private static void testExecutors() throws Throwable {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future> future = executor.submit(() -> {
while (true) {
System.out.println("执行1");
try {
Thread.sleep(1000); // 线程进入 TIMED_WAITING 状态,该状态遇到了 interrupt 线程中断的时候,会抛出 InterruptedException
System.out.println("执行2");
} catch (InterruptedException e) {
throw new InterruptedException();
} finally {
System.out.println("finally 执行3\n");
}
}
});
Thread.sleep(1500); // 等待 1.5 秒
List
future.get(); // 等待任务执行完毕
}
输出:
额外:区别于 shutdownNow
线程池还有一个 shutdown() 方法,该方法是将线程池状态改为 SHUTDOWN(拒绝新的任务,并且不会去停止在执行的任务执行,而是让他们正常执行关闭)
3、总结
优雅关闭线程的方法有以下几种:
interrupt():尝试去关闭线程volatile 关键字:中断标记shutdownNow():线程池相关方法,会去尝试关闭线程池中正在运行的线程。
上面这些方法都是优雅的关闭线程的方法,他们都是尝试去关闭线程而不是强制关闭线程。