遇见InterruptedException异常,怎么办?
前言
在Java语言的开发工作中,我们经常会碰到这样一类异常--InterruptedException(中断异常)。在绝大多数时候,我们的处理方式无非是catch注它,然后再输出异常信息,更或者是干脆直接忽略它了。那么这是否是一种正确的处理方式呢,要想搞清楚这件事,我们又必须要了解什么是InterruptedException,什么情况下会导致此异常的发生呢?本文笔者来简单讲述讲述这方面的内容,了解中断中断异常方面的知识将有助于我们在分布式的程序中处理这样的异常。
什么是中断异常?
现在一个首要的问题来了,什么是中断异常,InterruptedException到底意味着什么意思呢?下面是笔者通过阅读IBM官网上面对于此的定义:
When a method throws InterruptedException, it is telling you several things in addition to the fact that it can throw a particular checked exception. It is telling you that it is a blocking method and that it will make an attempt to unblock and return early
大致意思如下:InterruptedException实质上是一个检测异常,它表明又一个阻塞的被中断了,它尝试进行解除阻塞操作并返回地更早一些。中断阻塞方法的操作线程并不是自身线程干的,而是其它线程。而中断操作发生之后,随后会抛出一个InterruptedException,伴随着这个异常抛出的同时,当前线程的中断状态重新被置为false。这时,我们谈到了线程的中断状态,可能有些读者会有点晕了,下面我们来理理这段关系。
1.public static boolean interrupted(); // 检测当前线程是否已经中断,此方法会清除中断状态,也就是说,假设当前线程中断状态为true,第一次调此方法,将返回true,表明的确已经中断了,但是第二次调用后,将会返回true,因为第一次调用的操作已将中断状态重新置为false了。
2.public boolean isInterrupted() ; // 检测当前线程是否已经中断,此方法与上一方法的区别在于此方法不会清除中断状态。
3.public void interrupt(); //将线程中断状态设置为true,表明此线程目前是中断状态。此时如果调用isInterrupted方法,将会得到true的结果。
通过上述方法的解释,我们可以得出这样的一个结论:interrupt方法本质上不会进行线程的终止操作的,它不过是改变了线程的中断状态。而改变了此状态带来的影响是,部分可中断的线程方法(比如Object.wait, Thread.sleep)会定期执行isInterrupted方法,检测到此变化,随后会停止阻塞并抛出InterruptedException异常。但这是否意味着随后线程的退出呢?不是的,
下面是笔者对于此3个方法进行的一个简单测试demo。代码如下:
public class InterruptedException {
public static void main(String[] args) throws Exception {
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
InterruptedException异常的抛出并不是意味着线程必须得终止,它只是提醒当前线程有中断操作发生了,接下来怎么处理完全取决于线程本身,一般有3种处理方式:1.“吞并”异常,当做什么事都没发生过。
2.继续往外抛出异常。
3.其它方式处理异常。其它处理异常的方式就有很多种了,停止当前线程或者输出异常信息等等。
第三点其它中断异常处理的部分,将会是本文所要重点阐述的。这里面可有不少的讲究。
中断异常的处理
此部分的内容源自于笔者最近读过的一篇文章,IBM官网上关于JAVA理论与实践的一篇博文。里面包含了许多种情况下的处理方式首先我们来看第一种处理方式,可以说是最简单的一种,直接往外抛出InterruptedException,代码如下:
public class TaskQueue {
private static final int MAX_TASKS = 1000;
private BlockingQueue<Task> queue
= new LinkedBlockingQueue<Task>(MAX_TASKS);
public void putTask(Task r) throws InterruptedException {
queue.put(r);
}
public Task getTask() throws InterruptedException {
return queue.take();
}
}
但是很多情况下我们会主动捕获住这个异常,然后做进一步处理,比如说再throw它一次,如下代码所示:
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
Player playerOne, playerTwo;
try {
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// Just propagate the exception
throw e;
}
}
}
上面程序的意思是阻塞地等待2个运动员,最后再开始新的比赛,在等待运动员1和运动员2的过程中是有可能发生中断的,在这里我们的做法是进行一次简单的rethrow动作。其实这里会有2个问题,第一个如果方法在等待第二个运动员时发生中断异常了,如果就这么重抛,会导致数据的丢失,这里的丢失指的是运动员1的请求数据,因为在此处的运行过程中,运动员1的请求操作已经执行完成。这样的话,再下次重跑此任务的时候,还是会等待2次操作。所以,优化过后的代码如下:
public class PlayerMatcher {
...
public void matchPlayers() throws InterruptedException {
Player playerOne, playerTwo;
try {
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
还有一个问题是中断状态的问题,当InterruptedException抛出的时候,线程的中断状态将会被清除,重新置为false。此时最好的一种做法是将线程状态重新置为true,这样才能最完全的保留线程当前的执行状况,而调用的方法就是前面篇幅介绍的interrupt方法。下面是一个示例代码;
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
但是有的时候我们并不总是一遇到InterruptedException,就重置状态,一种好的做法是在程序最后执行完的时候,调用一次interrupt方法即可,捕获异常时只需要做个标记即可,如下代码。
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
// fall through and retry
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
}
中断状态被置为true了之后,当前线程会感知到此变化,对于后续的操作行为,完全由线程本身决定。这里会牵涉到Thread.currentThread().isInterrupted()的配合调用了。
与此做法完全相反的是,我们不能够随随便便把中断异常给“吞”了,比如说下面这种方式的代码:
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException swallowed) {
/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
}
}
}
那什么时候我们可以毫无顾虑地吞并这异常呢,这种情况也是有的,比如说当我们明确知道一个程序将要结束退出的时候。当然了,可能还有其它一些特殊场景,这个可以根据具体应用场景来定。
综述所述,线程中断异常的处理绝对不能简简单单地处理,处理不当会丢失掉许多重要的信息,不利于我们排除问题的原因。