CreateArtTechnology
/ Blog
Login
最新文章
Java
语言相关
库相关
虚拟机相关
CreateArtTechnology
项目搭建
使用的工具
自研的工具
开源工具
ELK
ElasticSearch
Jenkins
Markdown
GraphQL
Arthas
生产工具
Linux
Nginx
VersionControl
Subversion
Git
Redis
Archiva
Maven
Zookeeper
Spring
SpringBoot
MySql
HBase
Cassandra
容器化
Docker
Kubernetes
服务容器化从零开始
未分类笔记
算法相关
概念相关
豆知识
机器学习
机器学习从零开始
如何优雅地终止一个线程
21
2019-06-18 22:03:54
Java
语言相关
## 背景 在开发某个组件时,需要定期从数据库中拉取数据。由于整个逻辑非常简单,因此就启用了一个子线程(Thread)使用while循环+线程休眠来定期更新。 这时候我又想起一个老生常谈的问题——如何优雅地停止线程? ## 思路 大家都知道,Thread的stop方法早已废除,在高速上一脚猛刹,很可能人仰马翻,太危险。 时至今日,这个问题早已有常规解决方案,即检测线程的interrupt变量值对应中断状态(下简称interrupt状态)时停止循环,也就是类似如下的形式: ```java while(!中断状态) { // interrupt状态 // do sth... } ``` 这个方案的确非常常规,但每次到用的时候总会忧心忡忡——要知道跟线程interrupt状态相关的方法可是有多种,他们有什么区别?这样做能保证正常中断吗?Java进程运行结束的时候这个线程会终止吗(涉及到Tomcat的重启问题)? ### 中断相关的方法 **Thread.interrupted()** 实际上调用的是`Thread.currentThread().isInterrupted(true)`。 **Thread.currentThread().isInterrupted()** 实际上调用的是`Thread.currentThread()`这个对象的`isInterrupted()`。 **thread.isInterrupted()** 实际上调用的是`thread.isInterrupted(false)`。 **Thread.currentThread().interrupt()** 同样的,调用的是`Thread.currentThread()`这个对象的`interrupt()`方法。 **thread.interrupt()** 代码如下: ```java public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } ``` ###分析 本质上来说,在Thread类中线程中断相关的方法有3个: 1. Thread.interrupted() 2. thread.isInterrupted() 3. thread.interrupt() 尽管`Thread.interrupted()`和`thread.isInterrupted()`最终都是调用**Thread对象**也就是**thread**的isInterrupted方法,只是参数不同,但`thread.isInterrupted(boolean)`方法是个私有方法,即调用该方法只能通过`Thread.interrupted()`或`thread.isInterrupted()`。 没辙,还不如干脆就当做不同的方法来看。 ps. 显然,Thread.currentThread()对象肯定是目前在执行这个循环的线程对象,也就是谁调的这个方法所属的线程,包含主线程。 **thread.isInterrupted(boolean ClearInterrupted)** 这是一个native方法,注释中写道: > 检测某个线程是否被中断。ClearInterrupted参数决定了是否清理interrupt状态。 很好理解,调用`isInterrupted(true)`会返回当前的interrupt状态,并清理interrupt状态(即如果目前是中断状态,会修改为非中断状态)。 反之调用`isInterrupted(false)`不会清理interrupt状态。 **thread.interrupt()** 源码如前述,注释大意如下: > 中断这个Thread对象。 线程可以中断自身,但中断其他线程需要通过checkAccess()方法检查。(怎么检查的没细看) 如果线程在阻塞状态,会清除interrupt状态并抛出异常,不同的阻塞类型会抛出不同的异常,比如wait()、sleep()等会抛出InterruptedException;否则,会设置interrupt状态为中断状态。 源码调用了native方法interrupt0(),我们就不更深入分析了。从注释可以看出来线程是否在阻塞状态,会影响到interrupt()方法的行为: - 如果线程在阻塞状态,这个方法会清除interrupt状态并抛异常 - 如果线程未在阻塞状态,这个方法仅仅是设置了interrupt状态 **造成的影响** 让我们回到开头的解决方案。 ```java while(!中断状态) { // interrupt状态 // do sth... } ``` 总的来说,我们面临两个问题: 1. 如何合理获取interrupt状态? - `Thread.interrupted()`获取并清除状态 - `thread.isInterrupted()`获取并不清除状态 2. 如果while循环中有阻塞逻辑,会不会导致我们的解决方案有差异? - 阻塞时调用`thread.interrupt()`方法会清理interrupt状态并抛异常 - 非阻塞时调用`thread.interrupt()`设置interrupt状态并不抛异常 毫无疑问,获取interrupt状态两种方法都可以。但某些错误用法会导致线程无法中断。 **Bad Case1:错误使用Thread.interrupted(),导致线程无法中断** ```java while(!Thread.interrupted()) { // interrupt状态被清理,死循环 // do sth... if (Thread.interrupted()) { // 这里的判断清理了interrupt状态 // 做一些中断的后续动作 } } ``` 这种Case在使用正则表达式匹配(`matcher.find()`方法)时也要注意。 而在调用`thread.interrupt()`方法并搭配获取interrupt状态的方法时,就需要考虑阻塞问题了。 **Bad Case2:未考虑阻塞导致interrupt状态被吞,线程无法中断** ```java while(!Thread.interrupted()) { // interrupt状态被吞,死循环 // do sth... try { // 阻塞时调用interrupt()方法,只抛异常不设置interrupt状态 Thread.sleep(10); } catch (InterruptedException e) { // do sth... } } ``` **Bad Case3:仅使用interrupt()方法并不能中断线程** ```java while(true) { // 死循环 // do sth... try { Thread.sleep(10); } catch (InterruptedException e) { // do sth... // 仅使用interrupt()方法,就像调用System.gc()一样,只是进行通知设置状态,并不表示动作执行 Thread.currentThread().interrupt(); } } ``` ## 实验验证 ### 死循环3例 ```java // Bad Case 1 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { int i = 0; while(!Thread.interrupted() && i++ < 50000) { // interrupt状态被清理,死循环 System.out.println(i); if (Thread.interrupted()) { // 这里的判断清理了interrupt状态 System.out.println("interrupted"); } } } }); thread.start(); Thread.sleep(5); thread.interrupt(); // 中断 } // 输出结果:输出1~5w并在中间某处输出了interrupted,说明中断未生效,死循环 // Bad Case 2 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { int i = 0; while(!Thread.interrupted() && i++ < 50) { // interrupt状态被吞,死循环 System.out.println(i); try { // 阻塞时调用interrupt()方法,只抛异常不设置interrupt状态 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.start(); Thread.sleep(5); thread.interrupt(); } // 输出结果:输出1~50并在中间某处输出了异常信息,说明中断未生效,死循环 // Bad Case 3 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { int i = 0; while(true && i++ < 50) { // 死循环 System.out.println(i); try { Thread.sleep(1); } catch (InterruptedException e) { System.out.println("interrupted exception"); // 仅使用interrupt()方法,就像调用System.gc()一样,只是进行通知设置状态,并不表示动作执行 Thread.currentThread().interrupt(); } } } }); thread.start(); Thread.sleep(5); thread.interrupt(); } // 输出结果:从1~50中间开始轮番输出数字和"interrupted exception",说明中断未生效,死循环 ``` 验证结果符合预期。 ### 正确的结束方式4例 ```java // Case 1 无阻塞的情况 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { int i = 0; while(!Thread.interrupted()) { System.out.println(i++); } } }); thread.start(); Thread.sleep(50); thread.interrupt(); } // 输出结果:本机验证输出0~11013,成功中断 // Case 2 有阻塞的情况 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { int i = 0; while(!Thread.interrupted()) { System.out.println(i++); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); // 在这里调了一次interrupt(),保证线程未处于阻塞状态 Thread.currentThread().interrupt(); } } } }); thread.start(); Thread.sleep(50); thread.interrupt(); } // 输出结果:本机验证输出0~50并打印出InterruptedException,成功中断 // Case 3 使用volatile变量控制线程同步 public class Test extends Thread { // 利用volatile变量的机制 private volatile boolean stop; @Override public void run() { int i = 0; while(!stop) { System.out.println(i++); try { Thread.sleep(1); } catch (InterruptedException e) { System.out.println("interrupted exception"); } } } public static void main(String[] args) throws Exception { Test thread = new Test(); thread.start(); Thread.sleep(5); thread.stop = true; // 等5ms再调中断方法,确认在调interrupt方法时线程是否已中断 Thread.sleep(5); thread.interrupt(); } } // 输出结果:本机验证输出0~4,未输出"interrupted exception",成功控制线程终止 // Case 4 无脑但安全 public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { try { int i = 0; while (true) { System.out.println(i++); Thread.sleep(1); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); Thread.sleep(50); thread.interrupt(); } // 输出结果:本机验证输出0~49并打印出InterruptedException,成功中断 ``` ## 结论 1. 调用thread.interrupt()方法不一定能中断线程 2. 阻塞状态被中断会抛异常,但不会设置interrupt状态 3. interrupt状态设置为中断不代表线程没在运行,类似System.gc()只是通知一下,main方法也可以中断 4. Thread.interrupted()方法会清理interrupt状态 无阻塞的情况下要保证interrupt状态仅在while判断时重置,不能受其他部分影响。 ```java while(!Thread.interrupted()) { // interrupt状态,要保证循环中不会重置这个值 // do sth... } ``` 有阻塞的情况下要保证interrupt状态不被吞,可以在catch块中再次调用interrupt()方法设置interrupt状态。 ```java while(!Thread.interrupted()) { // interrupt状态,要保证循环中不会重置这个值 // do sth... try { Thread.sleep(100); } catch (InterruptedException e) { // do sth... // 在这里调了一次interrupt(),此时线程未处于阻塞状态,会设置interrupt状态 Thread.currentThread().interrupt(); } } ``` 还可以通过volatile变量控制线程中的循环,不过这种方式略微麻烦了些,如果不了解原理还很容易错,不太推荐使用。 当然,还可以无脑try catch,视情况而定,不太推荐使用。 ## 补充 ### 如何确保JVM关停时终止该线程 上述Case中的线程,如果不刻意中断,将会导致程序循环,无法正常结束(比如Tomcat的shutdown过程无法停止),只能强行关停(kill -9)Java进程。通过以下方法可以确保程序终止时终止该程序。 **使用守护线程** 就是调用`thread.setDaemon(true)`将线程设置为守护线程,会在主线程终止后自动终止。 注意**必须在线程启动前设置**。 **使用ShutdownHook** 通过添加中断逻辑到Hook中,可以在关闭程序(kill -15,ctrl+c)时运行这些中断逻辑。 ```java Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { if (!thread.isInterrupted()) { thread.interrupt(); } } })); ``` 不过直接调用thread.interrupt()都无法关闭的Bad Case,这种方式显然也无法关闭。 ## 参考资料 [利用 java.lang.Runtime.addShutdownHook() 钩子程序,保证java程序安全退出 - baibaluo - 博客园](https://www.cnblogs.com/baibaluo/p/3185925.html)
发布文章 101
文章被阅读 1817
最近修改
什么是“丝滑”的曲线
2021-12-08 15:19:20
高效空间数据索引R树及其批量加载方法STR简介
2021-09-29 20:33:37
关于分库分表的一些事儿
2021-06-25 11:51:25
获得诺奖的稳定匹配理论之TTC算法与GS算法
2021-03-14 23:04:48
算法小白的机器学习入门实践,从零到上线
2021-01-13 14:28:27
分站宗旨
一站式资料平台,减少重复检索,减少重复采坑。