这个故事可能很熟悉:您正在编写测试程序,并且需要暂停一段时间,因此您调用Thread.sleep()
。 但是随后,编译器或IDE却使您无法处理已检查的InterruptedException
。 什么是InterruptedException
,为什么要处理它?
对InterruptedException
的最常见的响应是吞下它-捕获它却什么也不做(或者记录它,这没有什么好用了)-正如我们在清单4中稍后看到的那样。 不幸的是,这种方法丢弃了有关发生中断这一事实的重要信息,这可能会损害应用程序取消活动或及时关闭的能力。
封锁方式
当一个方法抛出InterruptedException
,它不仅会告诉您一些事情,它还可以抛出一个特定的检查异常。 它告诉您这是一种阻止方法,如果您要求很好的话,它将尝试取消阻止并尽早返回。
阻塞方法不同于普通方法,后者只需花费很长时间即可运行。 普通方法的完成仅取决于您要求它执行的工作量以及是否有足够的计算资源(CPU周期和内存)可用。 另一方面,阻塞方法的完成还取决于某些外部事件,例如计时器到期,I / O完成或另一个线程的操作(释放锁,设置标志或在其上放置任务)工作队列)。 普通方法一旦完成工作便会完成,但是阻塞方法由于依赖于外部事件而难以预测。 阻塞方法可能会损害响应能力,因为很难预测何时完成。
由于如果阻塞方法永远不会发生,则阻塞方法可能会永远占用,因此对于取消阻塞操作通常很有用。 (取消长时间运行的非阻塞方法通常也很有用。)可取消操作是可以在通常自己完成时在外部将其移至完成的操作。 Thread
提供的并由Thread.sleep()
和Object.wait()
支持的中断机制是一种取消机制。 它允许一个线程请求另一个线程尽早停止正在执行的操作。 当一个方法抛出InterruptedException
,它告诉您,如果执行该方法的线程被中断,它将尝试停止正在执行的操作并提前返回并通过抛出InterruptedException
指示其提前返回。 行为良好的阻塞库方法应响应中断并抛出InterruptedException
以便可以在可取消的活动中使用它们而不会影响响应性。
线程中断
每个线程都有一个与之关联的布尔属性,该属性表示其中断状态 。 中断状态最初为假; 当某个线程通过调用Thread.interrupt()
被其他某个线程中断时,会发生以下两种情况之一。 如果该线程正在执行诸如Thread.sleep()
, Thread.join()
或Object.wait()
类的低级可中断阻塞方法,则它将取消阻塞并抛出InterruptedException
。 否则, interrupt()
仅设置线程的中断状态。 在被中断的线程中运行的代码以后可以轮询中断的状态,以查看是否已请求它停止正在执行的操作。 中断状态可以通过Thread.isInterrupted()
读取,也可以通过名称不佳的Thread.interrupted()
在单个操作中读取和清除。
中断是一种合作机制。 当一个线程中断另一个线程时,被中断的线程不一定会立即停止正在执行的操作。 相反,中断是一种在其方便时礼貌地要求另一个线程停止其正在执行的操作的方式。 某些方法(例如Thread.sleep()
会认真考虑此请求,但不需要方法注意中断。 不会阻塞但仍需要很长时间才能执行的方法可以通过轮询中断状态来尊重中断请求,如果中断则早点返回。 您可以随意忽略中断请求,但是这样做可能会影响响应能力。
协作的合作性质的好处之一是,它为安全构造可取消的活动提供了更大的灵活性。 我们很少希望活动立即停止; 如果活动在更新中被取消,则程序数据结构可能会处于不一致状态。 中断允许可取消的活动清除进行中的任何工作,还原不变式,将取消通知通知其他活动然后终止。
处理InterruptedException
如果抛出InterruptedException
意味着一个方法是一个阻塞方法,那么调用阻塞方法也意味着您的方法也是一个阻塞方法,并且您应该有一个处理InterruptedException
的策略。 最简单的策略通常是自己抛出InterruptedException
,如putTask()
中的putTask()
和getTask()
方法所示。这样做可以使您的方法也对中断做出响应,并且通常只需要在您的throws子句中添加InterruptedException
。
清单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 {
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
并不是一种选择,例如Runnable
定义的任务调用可中断方法时。 在这种情况下,您不能抛出InterruptedException
,但是您也不想做任何事情。 当阻塞方法检测到中断并引发InterruptedException
,它将清除中断状态。 如果捕获了InterruptedException
但无法将其抛出,则应保留发生中断的证据,以便调用堆栈中更高级别的代码可以了解该中断并在需要时对其进行响应。 可以通过调用interrupt()
“重新interrupt()
”当前线程来完成此任务,如清单3所示。至少,每当您捕获InterruptedException
且不抛出该异常时,请在返回之前重新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 statusThread.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
)工作线程实现是对中断的响应,因此中断在线程池中运行的任务可能具有取消任务和通知执行线程线程池正在关闭的作用。 如果任务是吞下中断请求,则工作线程可能不会得知请求了中断,这可能会延迟应用程序或服务的关闭。
实施可取消的任务
语言规范中没有任何内容可以赋予中断任何特定的语义,但是在较大的程序中,除了取消之外,很难为中断保留任何语义。 根据活动,用户可以通过GUI或通过网络机制(例如JMX或Web服务)请求取消。 程序逻辑也可以请求它。 例如,如果Web搜寻器检测到磁盘已满,则它可能会自动关闭自身,或者并行算法可能启动多个线程来搜索解决方案空间的不同区域,并在其中一个找到解决方案后将其取消。
仅仅因为一个任务是取消并不意味着它需要一个中断请求立即响应。 对于在循环中执行代码的任务,通常每个循环迭代仅检查一次中断。 根据循环执行的时间长短,任务代码可能会花费一些时间来通知线程已被中断(通过使用Thread.isInterrupted()
轮询中断状态或调用阻塞方法)。 如果任务需要更快速地响应,它可以更频繁地轮询中断状态。 阻塞方法通常在进入时立即轮询中断状态,如果设置为提高响应速度,则抛出InterruptedException
。
吞下中断的一次可接受的时间是您知道线程即将退出时。 仅当调用中断方法的类是Thread
一部分而不是Runnable
或通用库代码的一部分时,才发生这种情况,如清单5所示。它创建一个枚举素数的线程,直到被中断为止,并允许该线程中断时退出。 寻优循环在两个地方检查中断:一次是通过轮询while循环头中的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
的非阻塞I / O类也不支持可中断的I / O,但是可以通过关闭通道或在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
并不要将其重新抛出,也请确保恢复中断状态,以使调用它的代码不会失去对发生中断的了解。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp05236/index.html