Java学习之InterruptedException异常处理方式
前言
-
现象描述
在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 方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。
Thread中断相关方法
使用样例
-
方法作用
/** * @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
interrupt()
-
作用分析
将线程中断状态设置为true,表明此线程目前是中断状态。此时如果调用isInterrupted方法,将会得到true的结果。
-
注意事项
线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted() 和 isInterrupted()
-
源码分析
// 检测当前线程是否已经中断,此方法会清除中断状态,也就是说,假设当前线程中断状态为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()方法)。
举例说明
interrupt ≠ 终止操作
-
样例源码
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...
使用thread.interrupt()中断非阻塞状态线程
-
方案描述
上面是中断一个非阻塞状态的线程的常见做法,但对非检测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()中断阻塞状态线程
-
方案描述
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