背景
在开发某个组件时,需要定期从数据库中拉取数据。由于整个逻辑非常简单,因此就启用了一个子线程(Thread)使用while循环+线程休眠来定期更新。
这时候我又想起一个老生常谈的问题——如何优雅地停止线程?
思路
大家都知道,Thread的stop方法早已废除,在高速上一脚猛刹,很可能人仰马翻,太危险。
时至今日,这个问题早已有常规解决方案,即检测线程的interrupt变量值对应中断状态(下简称interrupt状态)时停止循环,也就是类似如下的形式:
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()
代码如下:
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个:
- Thread.interrupted()
- thread.isInterrupted()
- 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状态
造成的影响
让我们回到开头的解决方案。
while(!中断状态) { // interrupt状态
// do sth...
}
总的来说,我们面临两个问题:
- 如何合理获取interrupt状态?
-
Thread.interrupted()获取并清除状态 -
thread.isInterrupted()获取并不清除状态
- 如果while循环中有阻塞逻辑,会不会导致我们的解决方案有差异?
- 阻塞时调用
thread.interrupt()方法会清理interrupt状态并抛异常 - 非阻塞时调用
thread.interrupt()设置interrupt状态并不抛异常
毫无疑问,获取interrupt状态两种方法都可以。但某些错误用法会导致线程无法中断。
Bad Case1:错误使用Thread.interrupted(),导致线程无法中断
while(!Thread.interrupted()) { // interrupt状态被清理,死循环
// do sth...
if (Thread.interrupted()) { // 这里的判断清理了interrupt状态
// 做一些中断的后续动作
}
}
这种Case在使用正则表达式匹配(matcher.find()方法)时也要注意。
而在调用thread.interrupt()方法并搭配获取interrupt状态的方法时,就需要考虑阻塞问题了。
Bad Case2:未考虑阻塞导致interrupt状态被吞,线程无法中断
while(!Thread.interrupted()) { // interrupt状态被吞,死循环
// do sth...
try {
// 阻塞时调用interrupt()方法,只抛异常不设置interrupt状态
Thread.sleep(10);
} catch (InterruptedException e) {
// do sth...
}
}
Bad Case3:仅使用interrupt()方法并不能中断线程
while(true) { // 死循环
// do sth...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// do sth...
// 仅使用interrupt()方法,就像调用System.gc()一样,只是进行通知设置状态,并不表示动作执行
Thread.currentThread().interrupt();
}
}
实验验证
死循环3例
// 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例
// 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,成功中断
结论
- 调用thread.interrupt()方法不一定能中断线程
- 阻塞状态被中断会抛异常,但不会设置interrupt状态
- interrupt状态设置为中断不代表线程没在运行,类似System.gc()只是通知一下,main方法也可以中断
- Thread.interrupted()方法会清理interrupt状态
无阻塞的情况下要保证interrupt状态仅在while判断时重置,不能受其他部分影响。
while(!Thread.interrupted()) { // interrupt状态,要保证循环中不会重置这个值
// do sth...
}
有阻塞的情况下要保证interrupt状态不被吞,可以在catch块中再次调用interrupt()方法设置interrupt状态。
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)时运行这些中断逻辑。
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 - 博客园
本文搬自我的博客,欢迎参观!