很多 Java™ 语言方法,例如​​Thread.sleep()​​​​Object.wait()​​,都可以抛出​​InterruptedException​​。您不能忽略这个异常,因为它是一个检查异常(checked exception)。但是应该如何处理它呢?在本月的 ​Java 理论与实践中​,并发专家 Brian Goetz 将解释 InterruptedException 的含义,为什么会抛出 InterruptedException,以及在捕捉到该异常时应该怎么做。

这样的情景您也许并不陌生:您在编写一个测试程序,程序需要暂停一段时间,于是调用​​Thread.sleep()​​。但是编译器或 IDE 报错说没有处理检查到的​​InterruptedException​​。​​InterruptedException​​ 是什么呢,为什么必须处理它?对于​​InterruptedException​​,一种常见的处理方式是 “生吞(swallow)” 它 —— 捕捉它,然后什么也不做(或者记录下它,不过这也好不到哪去)—— 就像后面的 ​​清单 4​​ 一样。不幸的是,这种方法忽略了这样一个事实:这期间可能发生中断,而中断可能导致应用程序丧失及时取消活动或关闭的能力。

阻塞方法

当一个方法抛出​​InterruptedException​​ 时,它不仅告诉您它可以抛出一个特定的检查异常,而且还告诉您其他一些事情。例如,它告诉您它是一个阻塞(blocking)方法,如果您响应得当的话,它将尝试消除阻塞并尽早返回。

阻塞方法不同于一般的要运行较长时间的方法。一般方法的完成只取决于它所要做的事情,以及是否有足够多可用的计算资源(CPU 周期和内存)。而阻塞方法的完成还取决于一些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作(释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中)。一般方法在它们的工作做完后即可结束,而阻塞方法较难于预测,因为它们取决于外部事件。阻塞方法可能影响响应能力,因为难于预测它们何时会结束。

阻塞方法可能因为等不到所等的事件而无法终止,因此令阻塞方法可取消​Thread​​​​Thread.sleep()​​​​Object.wait()​​​​InterruptedException​​​​InterruptedException​​​​InterruptedException​​,以便能够用于可取消活动中,而不至于影响响应。

线程中断

每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用​​Thread.interrupt()​​​​Thread.sleep()​​、​​Thread.join()​​​​Object.wait()​​,那么它将取消阻塞并抛出​​InterruptedException​​。否则,​​interrupt()​​​​Thread.isInterrupted()​​​​Thread.interrupted()​​ 的操作读取和清除。中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如​​Thread.sleep()​​,很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。

中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

 

处理 InterruptedException

如果抛出​​InterruptedException​​​​InterruptedException​​。通常最容易的策略是自己抛出​​InterruptedException​​,如清单 1 中​​putTask()​​​​getTask()​​​​InterruptedException​​ 添加到 throws 子句。

清单 1. 不捕捉 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();
}
}

有时候需要在传播异常之前进行一些清理工作。在这种情况下,可以捕捉​​InterruptedException​​,执行清理,然后抛出异常。清单 2 演示了这种技术,该代码是用于匹配在线游戏服务中的玩家的一种机制。​​matchPlayers()​​​​InterruptedException​​,这样那个玩家对游戏的请求就不至于丢失。

清单 2. 在重新抛出 InterruptedException 之前执行特定于任务的清理工作
public class PlayerMatcher {
private PlayerSource players;

public PlayerMatcher(PlayerSource players) {
this.players = players;
}

public void matchPlayers() throws InterruptedException {
try {
Player playerOne, playerTwo;
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​​​​Runnable​​​​InterruptedException​​,但是您也不想什么都不做。当一个阻塞方法检测到中断并抛出​​InterruptedException​​​​InterruptedException​​​​interrupt()​​​​InterruptedException​​并且不重新抛出它时,就在返回之前重新中断当前线程。

清单 3. 捕捉 InterruptedException 后恢复中断状态
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​​​​InterruptedException​​ 也不是明智的做法,因为等到人来读取日志的时候,再来对它作出处理就为时已晚了。) 清单 4 展示了一种使用得很广泛的模式,这也是生吞中断的一种模式:

清单 4. 生吞中断 —— 不要这么做
// Don't do this 
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 */
}
}
}

如果不能重新抛出​​InterruptedException​​,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。标准线程池 (​​ThreadPoolExecutor​​)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知执行线程线程池正要关闭。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。

实现可取消任务

语言规范中并没有为中断提供特定的语义,但是在较大的程序中,难于维护除取消外的任何中断语义。取决于是什么活动,用户可以通过一个 GUI 或通过网络机制,例如 JMX 或 Web 服务来请求取消。程序逻辑也可以请求取消。例如,一个 Web 爬行器(crawler)如果检测到磁盘已满,它会自动关闭自己,否则一个并行算法会启动多个线程来搜索解决方案空间的不同区域,一旦其中一个线程找到一个解决方案,就取消那些线程。

仅仅因为一个任务是可取消的,并不意味着需要立即​Thread.isInterrupted()​​​​InterruptedException​​。惟一可以生吞中断的时候是您知道线程正要退出。只有当调用可中断方法的类是​​Thread​​​​Runnable​​​​isInterrupted()​​​​BlockingQueue.put()​​。

清单 5. 如果知道线程正要退出的话,则可以生吞中断
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;

PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}

public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* Allow thread to exit */
}
}

public void cancel() { interrupt(); }
}

不可中断的阻塞方法

并非所有的阻塞方法都抛出​​InterruptedException​​。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出​​InterruptedException​​,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个​​SocketException​​。​​java.nio​​​​Selector​​​​synchronized​​​​ReentrantLock​​ 支持可中断的获取模式。

不可取消的任务

有些任务拒绝被中断,这使得它们是不可取消的。但是,即使是不可取消的任务也应该尝试保留中断状态,以防在不可取消的任务结束之后,调用栈上更高层的代码需要对中断进行处理。清单 6 展示了一个方法,该方法等待一个阻塞队列,直到队列中出现一个可用项目,而不管它是否被中断。为了方便他人,它在结束后在一个 finally 块中恢复中断状态,以免剥夺中断请求的调用者的权利。(它不能在更早的时候恢复中断状态,因为那将导致无限循环 ——​​BlockingQueue.take()​​​​InterruptedException​​。)

清单 6. 在返回前恢复中断状态的不可取消任务
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();
}
}

结束语

您可以用 Java 平台提供的协作中断机制来构造灵活的取消策略。各活动可以自行决定它们是可取消的还是不可取消的,以及如何对中断作出响应,如果立即返回会危害应用程序完整性的话,它们还可以推迟中断。即使您想在代码中完全忽略中断,也应该确保在捕捉到​​InterruptedException​​ 但是没有重新抛出它的情况下,恢复中断状态,以免调用它的代码无法获知中断的发生。

 

获得产品和技术

 

​http://www.ibm.com/developerworks/cn/java/j-jtp05236.html​