现象描述
在Java语言的开发工作中,我们经常会碰到这样一类异常–InterruptedException(中断异常)。在绝大多数时候,我们的处理方式无非是catch住它,然后再输出异常信息,更或者是干脆直接忽略它了。那么这是否是一种正确的处理方式呢,要想搞清楚这件事,我们又必须要了解什么是InterruptedException,什么情况下会导致此异常的发生呢?
内容简介
中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false。
中断原理
Java 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
Java 中断模型也是这么简单,每个线程对象里都有一个 boolean 类型的标识(不一定就要是 Thread 类的字段,实际上也的确不是,这几个方法最终都是通过 native 方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。
例如,当线程 t1 想中断线程 t2,只需要在线程 t1 中将线程 t2 对象的中断标识置为 true,然后线程 2 可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
IBM官网定义
InterruptedException实质上是一个检测异常,它表明有一个阻塞的被中断了,它尝试进行解除阻塞操作并返回地更早一些。中断阻塞方法的操作线程并不是自身线程干的,而是其它线程。而中断操作发生之后,随后会抛出一个InterruptedException,伴随着这个异常抛出的同时,当前线程的中断状态重新被置为false。
何时抛出
当阻塞方法收到中断请求的时候就会抛出InterruptedException异常。
哪些方法抛出
类库中的有些类的方法也可能会调用中断,如 FutureTask 中的 cancel 方法,如果传入的参数为 true,它将会在正在运行异步任务的线程上调用 interrupt 方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么 cancel 方法中的参数将不会起到什么效果;
又如 ThreadPoolExecutor 中的 shutdownNow 方法会遍历线程池中的工作线程并调用线程的 interrupt 方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。
方法作用
/**
* @author charlesYan
* @Description: 测试线程中断相关方法
* @date 2022年03月23日
*/
public class InterruptedExceptionTest {
public static void main(String[] args) {
System.out.println("初始中断状态:" + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("执行完interrupt方法后,中断状态:" + Thread.currentThread().isInterrupted());
System.out.println("首次调用interrupted方法返回结果:" + Thread.currentThread().interrupted());
System.out.println("此时中断状态:" + Thread.currentThread().isInterrupted());
System.out.println("第二次调用interrupted方法返回结果:" + Thread.currentThread().interrupted());
System.out.println("此时中断状态:" + Thread.currentThread().isInterrupted());
}
}
输出结果
初始中断状态:false
执行完interrupt方法后,中断状态:true
首次调用interrupted方法返回结果:true
此时中断状态:false
第二次调用interrupted方法返回结果:false
此时中断状态:false
作用分析
将线程中断状态设置为true,表明此线程目前是中断状态。此时如果调用isInterrupted方法,将会得到true的结果。
注意事项
线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
源码分析
// 检测当前线程是否已经中断,此方法会清除中断状态,也就是说,假设当前线程中断状态为true,第一次调此方法,将返回true,表明的确已经中断了,但是第二次调用后,将会返回false,因为第一次调用的操作已将中断状态重新置为false了。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 检测当前线程是否已经中断,此方法与上一方法的区别在于此方法不会清除中断状态。
public boolean isInterrupted() {
return isInterrupted(false);
}
主要区别
interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程(线程对象对应的线程不一定是当前运行的线程:例如我们可以在A线程中去调用B线程对象的isInterrupted方法)。
// ClearInterrupted我们就能知道,这个参数代表是否要清除状态位。如果这个参数为true,说明返回线程的状态位后,要清掉原来的状态位(恢复成原来情况)。这个参数为false,就是直接返回线程的状态位。
private native boolean isInterrupted(boolean ClearInterrupted);
这两个方法很好区分,只有当前线程才能清除自己的中断位(对应interrupted()方法)。
样例源码
public class InterruptedExceptionTest {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new Worker());
thread.start();
Thread.sleep(200);
thread.interrupt();
System.out.println("Main thead stopped..");
}
public static class Worker implements Runnable{
@Override
public void run() {
System.out.println("Worker started...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Worker is interrupted:" + Thread.currentThread().isInterrupted());
}
System.out.println("Worker stopped...");
}
}
}
输出结果
Worker started...
Main thead stopped..
Worker is interrupted:false
Worker stopped...
结论
interrupt方法本质上不会进行线程的终止操作的,它不过是改变了线程的中断状态。而改变了此状态带来的影响是,部分可中断的线程方法(比如Object.wait, Thread.sleep)会定期执行isInterrupted方法,检测到此变化,JVM会将线程的中断标志重新设置为false,随后会停止阻塞并抛出InterruptedException异常。总之,interrupt的作用就是需要用户自己去监视线程的状态位并做处理。
但InterruptedException异常的抛出并不是意味着线程必须得终止,它只是提醒当前线程有中断操作发生了,接下来怎么处理完全取决于线程本身,一般有3种处理方式:
"吞并"异常,当做什么事都没发生过。
继续往外抛出异常。
其它方式处理异常(其它处理异常的方式就有很多种了,停止当前线程或者输出异常信息等等)。
内容简介
当可能阻塞的方法声明中有抛出 InterruptedException 则暗示该方法是可中断的,如 BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep 等,如果程序捕获到这些可中断的阻塞方法抛出的 InterruptedException 或检测到中断后,这些中断信息该如何处理?一般有以下两个通用原则:
向上抛出
如果遇到的是可中断的阻塞方法抛出 InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断,则可清除中断状态并抛出 InterruptedException,使当前方法也成为一个可中断的方法。
捕获异常
若有时候不太方便在方法上抛出 InterruptedException,比如要实现的某个接口中的方法签名上没有 throws InterruptedException,这时就可以捕获可中断方法的 InterruptedException 并通过 Thread.currentThread.interrupt() 来重新设置中断状态。
一般的代码中,尤其是作为一个基础类库时,绝不应当吞掉中断,即捕获到 InterruptedException 后在 catch 里什么也不做,清除中断状态后又不重设中断状态也不抛出 InterruptedException 等。因为吞掉中断状态会导致方法调用栈的上层得不到这些信息。
当然,凡事总有例外的时候,当你完全清楚自己的方法会被谁调用,而调用者也不会因为中断被吞掉了而遇到麻烦,就可以这么做。
常见场景
有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用 interrupt 会发生些什么都是事先知道的:如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的 interrupt 后该线程会做出什么样的响应,那就不应当中断该线程。
方案描述
这是最基础中断形式,某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求,这种线程的run方法遵循如下形式:
样例源码
public void run() {
try {
...
/*
* 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
* !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
* 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
*/
while (!Thread.currentThread().isInterrupted()&& more work to do) {
do more work
}
} catch (InterruptedException e) {
//线程在wait或sleep期间被中断了
} finally {
//线程结束前做一些清理工作
}
}
方案描述
上面是while循环在try块里,如果try在while循环里时,应该在catch块里重新设置一下中断表示位,因为抛出InterruptedException异常后,中断标示位会自动清除。
样例源码
public void run() {
while (!Thread.currentThread().isInterrupted()&& more work to do) {
try {
...
sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();//重新设置中断标示
}
}
}
捕获不处理
不要在你的底层代码里捕获InterruptedException异常后不处理,会处理不当。
void mySubTask(){
try{
sleep(delay);
}catch(InterruptedException e){}//不要这样做
}
如果你不知道抛InterruptedException异常后如何处理,那么你有如下好的建议处理方式:
恢复中断状态
在catch子句中,调用Thread.currentThread.interrupt()来设置中断状态(因为抛出异常后中断标示会被清除),让外界通过判断Thread.currentThread().isInterrupted()标示来决定是否终止线程还是继续下去。
void mySubTask() {
...
try {
sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().isInterrupted();
}
...
}
直接抛出异常
void mySubTask() throws InterruptedException {
...
sleep(delay);
...
}
小技巧
如果你不知道如何处理异常,外界有需要知道这个异常,就把他抛出去。
方案描述
中断线程最好的,最受推荐的方式是:使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。
这里需注意一点的是需将共享变量定义成volatile 类型或将对它的一切访问封入同步的块/方法(synchronized blocks/methods)中。
样例源码
class Example2 extends Thread {
volatile boolean stop = false;// 线程中断信号量
public static void main(String args[]) throws Exception {
Example2 thread = new Example2();
System.out.println("Starting thread...");
thread.start();
Thread.sleep(3000);
System.out.println("Asking thread to stop...");
// 设置中断信号量
thread.stop = true;
Thread.sleep(3000);
System.out.println("Stopping application...");
}
public void run() {
// 每隔一秒检测一下中断信号量
while (!stop) {
System.out.println("Thread is running...");
long time = System.currentTimeMillis();
/*
* 使用while循环模拟 sleep 方法,这里不要使用sleep,否则在阻塞时会 抛
* InterruptedException异常而退出循环,这样while检测stop条件就不会执行,
* 失去了意义。
*/
while ((System.currentTimeMillis() - time < 1000)) {}
}
System.out.println("Thread exiting under request...");
}
}
输出结果
Starting thread...
Thread is running...
Thread is running...
Thread is running...
Thread is running...
Asking thread to stop...
Thread exiting under request...
Stopping application...
方案描述
上面是中断一个非阻塞状态的线程的常见做法,但对非检测isInterrupted()条件会更简洁。
样例源码
class Example2 extends Thread {
public static void main(String args[]) throws Exception {
Example2 thread = new Example2();
System.out.println("Starting thread...");
thread.start();
Thread.sleep(3000);
System.out.println("Asking thread to stop...");
// 发出中断请求
thread.interrupt();
Thread.sleep(3000);
System.out.println("Stopping application...");
}
public void run() {
// 每隔一秒检测是否设置了中断标示
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread is running...");
long time = System.currentTimeMillis();
// 使用while循环模拟 sleep
while ((System.currentTimeMillis() - time < 1000) ) {
// 这里应该短暂的 sleep,避免对 CPU 消耗过大
}
}
System.out.println("Thread exiting under request...");
}
}
小结
但是,当线程等待某些事件发生而被阻塞,又会发生什么?
当然,如果线程被阻塞,它便不能核查共享变量,也就不能停止。这在许多情况下会发生,例如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()时,都可能永久的阻塞线程。即使发生超时,在超时期满之前持续等待也是不可行和不适当的,所以,要使用某种机制使得线程更早地退出被阻塞的状态。
方案描述
Thread.interrupt()方法不会中断一个正在运行的线程。
这一方法实际上完成的是,设置线程的中断标示位,在线程受到阻塞的地方(如调用sleep、wait、join等地方)抛出一个异常InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。
样例源码
class Example3 extends Thread {
public static void main(String args[]) throws Exception {
Example3 thread = new Example3();
System.out.println("Starting thread...");
thread.start();
Thread.sleep(3000);
System.out.println("Asking thread to stop...");
thread.interrupt();// 等中断信号量设置后再调用
Thread.sleep(3000);
System.out.println("Stopping application...");
}
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread running...");
try {
/*
* 如果线程阻塞,将不会去检查中断信号量stop变量,所 以thread.interrupt()
* 会使阻塞线程从阻塞的地方抛出异常,让阻塞线程从阻塞状态逃离出来,并
* 进行异常块进行 相应的处理
*/
Thread.sleep(1000);// 线程阻塞,如果线程收到中断操作信号将抛出异常
} catch (InterruptedException e) {
System.out.println("Thread interrupted...");
/*
* 如果线程在调用 Object.wait()方法,或者该类的 join() 、sleep()方法
* 过程中受阻,则其中断状态将被清除
*/
System.out.println(this.isInterrupted());// false
//中不中断由自己决定,如果需要真真中断线程,则需要重新设置中断位,如果
//不需要,则不用调用
Thread.currentThread().interrupt();
}
}
System.out.println("Thread exiting under request...");
}
}
输出结果
Starting thread...
Thread is running...
Thread is running...
Thread is running...
Asking thead to stop...
Thread interrupted...
false
Thread exiting under request...
Stopping application...
结果分析
一旦Example3中的Thread.interrupt()被调用,线程便收到一个异常,于是逃离了阻塞状态并确定应该停止。
上面我们还可以使用共享信号量来替换!Thread.currentThread().isInterrupted()条件,但不如它简洁。
没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。
对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。
不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
遇见InterruptedException异常,怎么办?
https://blog.csdn.net/Androidlushangderen/article/details/54984681
不学无数——InterruptedException异常处理
https://www.jianshu.com/p/a8abe097d4ed
JCIP-18-thread InterruptedException 中断异常处理及中断机制
https://houbb.github.io/2019/01/18/jcip-18-thread-interrupt