为什么InterruptedException会清除线程的中断状态?

23
如果在调用Object.wait()Thread.join()期间线程被中断,它会抛出InterruptedException异常,并重置线程的中断状态。也就是说,如果我在Runnable.run()方法中有如下循环:
while (!this._workerThread.isInterrupted()) {
    // do something
    try {
        synchronized (this) {
            this.wait(this._waitPeriod);
        }
    } catch (InterruptedException e) {
        if (!this._isStopping()) {
            this._handleFault(e);
        }
    }
}

在调用 interrupt() 后,线程将继续运行。这意味着我必须在循环条件中检查自己的停止标志、重新抛出异常或添加 break 来明确地跳出循环。

现在,这并不是一个问题,因为这种行为已经被充分记录,也不会阻止我按照自己的方式进行操作。然而,我似乎不理解它背后的概念:为什么一旦抛出异常,线程就不再被认为是中断状态?如果你使用 interrupted() 而不是 isInterrupted() 来获取中断状态,也会出现类似的情况,线程只会在第一次出现中断。

我在这里做了什么不寻常的事情吗?例如,捕获 InterruptedException 是否更常见于循环外部?

(尽管我不完全是初学者,但我将其打上了“初学者”的标签,因为对我来说,这似乎是一个非常基本的问题。)


由于您所询问的是一个观点问题(“为什么会这样?”),您应该将其设为社区维基。 - Jonathan Feinberg
1
@Jonathan:这并不是一个观点问题。从API文档来看,很明显这背后必须有一定的理由,因此我的问题是有正确答案的。我接受的那个听起来非常合理(线程安全的中断处理)。 - Hanno Fietz
4个回答

15

这个想法是,一个中断应该只被处理一次。如果显式的 InterruptedException 没有清除 "interrupt" 标志,那么大多数对 InterruptedException 的捕获者都必须显式地清除该标志。反之,您可以通过自我中断 (Thread.currentThread().interrupt()) 来 "取消" 标志。Java 的设计师选择了语义,以便大多数情况下节省击键次数 (即您更经常地想要清除标志而不是保持它被设置)。


2
我并不反对这里所进行的逻辑,但他们肯定应该更好地命名这个方法。 - John Vint

4
它不应该这样。这是一个不幸的设计缺陷,过于依赖中断会有风险,因为太多的库代码会捕获InterruptedException,但却没有重置线程的中断标志并继续执行。如果在你的代码执行时发出中断信号,而此时上述有问题的库代码正在运行,当你的代码重新获得执行控制权时,它将毫无头绪地停留在中断已经发生的状态下。
只要在任何调用你的代码的地方发生一次,为了能够安全地中断线程并使用中断位从内部控制流程,你需要100%确定你调用的每个代码都没有错误地清除中断位。当涉及到库时,这非常难以做到,但即使你可以解决你的代码中使用的每个库的问题,仍然不能解决有缺陷的 JRE 代码可能会犯同样的错误的情况。
事实上,只需一个库(或 JRE!)的作者不关心或不考虑中断就可以破坏需要中断的代码逻辑,这表明这是错误的默认操作。那些不关心线程中断状态的人可能不会在捕获InterruptedException后重置它——也许他们甚至不知道它的存在!如果捕获InterruptedException不重置线程的中断状态,那么任何不知道中断位存在的人都会自动“做正确的事情”,并且不会对依赖中断的任何调用代码造成问题。需要清除它的人仍然可以手动清除,但这将是一项显式操作,比意外捕获检查的InterruptedException异常造成的副作用更可靠。目前,如果你依赖于线程的中断位,则在调用堆栈上的任何人轻率地调用Thread.sleep()都可能毁了你的一天。
因此,大多数 Java 多线程代码将只需使用“正在运行”的实例字段和一些机制来模拟 Java 线程中断模型。

1

如果你这样编写代码,就不需要使用标志了:

try {
    while (!this._workerThread.isInterrupted()) {
        // do something
        synchronized (this) {
            this.wait(this._waitPeriod);
        }
        // do something else
    }
} catch (InterruptedException e) {
    // ignore ...
}

正如@Boyan所指出的那样,通常来说压制中断异常是一个不好的主意。在这种情况下,上下文将决定您是否应该压制它(如上所述),重新设置中断标志或允许异常传播。除其他因素外,这取决于中断在您的应用程序中/对应什么含义。

你不应该忽略它。在90%的情况下,你编写的代码完全没有问题,但是有可能会有其他人正在等待这个中断,清除标志将会破坏他们的逻辑。因此,为了安全起见,在异常处理程序中必须调用Thread.currentThread().interrupt() - Boyan

-1

这是因为InterruptedException被认为是一种异常事件,即有人试图从外部停止线程。

当您想要真正中断线程时,只需通过设置布尔值或类似的方式来打破其循环条件。或者您可以在该线程内部使用.wait().notify()。但是,如果您正在外部执行wait()

  • 将抛出异常以通知外部线程尝试中断我或使我等待
  • 线程继续工作,因为它不接受来自另一个线程的任何命令!但是,异常的引发允许您添加特殊处理并做任何您想做的事情,也有效地停止了线程。

这是错误的。Thread#isInterrupted 是你所说的布尔值,可以用来循环。中断线程会设置此位,但捕获 InterruptedException 会清除它,这就是这个问题的原因。如果您完全控制该线程,则可以自行决定要执行什么操作,但如果您正在编写不执行任何线程管理而仅由其他代码调用的库或实用程序代码,则重置刚刚设置的中断位非常不礼貌。 - mpontes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接