为什么要同时使用布尔型变量和interrupt()方法来信号线程终止?

7
我正在阅读关于在https://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html这个链接中结束线程的方法,这个链接是我从问题如何在Java中终止线程?中找到的。
首先,在第一个链接中,他们讨论了使用volatile变量向线程发出终止信号。线程应该检查此变量,并在变量具有表示终止的值时停止操作(例如,如果它为null)。因此,要终止线程,您需要将该变量设置为null
然后,他们讨论了添加中断以帮助处理长时间阻塞的线程。他们给出了以下示例的stop()方法,该方法将易失性变量(waiter)设置为null,然后还引发了中断。
public void stop() {
    Thread moribund = waiter;
    waiter = null;
    moribund.interrupt();
}

我只是在想,为什么你需要两个方法一起用?为什么不只使用interrupt()方法,然后正确地处理它呢?对我来说,这似乎有些多余。


中断可以来自不同的来源,例如操作系统。在这些情况下,您不希望线程退出。 - lordoku
1
那个问题上的很多答案都是误导性的或者只是不知情的坏建议。一个好的答案是http://stackoverflow.com/q/11387729 - Nathan Hughes
2
@lordoku: 顺便说一下,OS中断在这里不应该有影响,除非你额外努力才能使OS中断中止Java线程,请参见Java硬件中断处理如何优雅地处理Java中的sigkill信号。这应该严格关于Java线程中断。 - Nathan Hughes
@NathanHughes 好主意。我在错误地回忆大学的某些事情。 - lordoku
3个回答

5

(首先,这是一般性的描述,可以说我没有注意到问题的具体细节。跳到结尾部分,那里讨论了问题中涉及的技术规范。)

没有好的技术原因。这部分是关于人类限制和混乱的API设计。

首先考虑应用程序开发人员的优先事项是创建解决业务问题的工作代码。在赶工期的过程中,彻底学习这样的低级API会被忽略。

其次,在学习东西时,有一个倾向就是达到足够好的状态就不再深入学习了。像线程和异常处理之类的主题往往被忽视。中断是线程书籍中的后面的主题。

现在想象一个由多个人使用的代码库,他们的技能水平和注意力不同,可能会忘记从wait和sleep抛出InterruptedException会重置中断标志,或者interrupted不等于isInterrupted,或者InterruptedIoException也会重置中断标志。如果您有一个finally块捕获IOException,您可能会忽略InterruptedException是IOException的子类,因此可能无法恢复中断标志。可能是匆忙的人决定放弃它,我不能依靠这个中断标志。

这样做对吗?不是的。

  • 手动标志无法像中断一样帮助短路等待或睡眠。

  • Java 5并发工具期望任务使用中断来取消。执行器期望提交给它们的任务检查中断以便优雅地退出。您的任务可能会使用其他组件,例如阻塞队列。该队列需要能够响应中断,使用它的东西需要意识到中断。手动标志在这里无法工作,因为Java 5类无法知道它。

由于使用期望中断的工具,但由于难以管理的技术问题而没有信心标志值,将导致出现此类代码。

(抱怨结束,现在实际回答特定的技术规范示例)

好的,看看这篇技术指南文章。有三件事引人注目:

1)除了缩短睡眠时间之外,它根本没有利用中断。然后它只是压制异常,不费力恢复标志或检查它。您可以使用InterruptedException通过在while循环外捕获它来终止,但这不是这个示例所做的。这似乎是一种奇怪的方式。

2)随着示例的完善,清楚地表明正在使用标志来打开和关闭等待。有人可能会使用中断来做到这一点,但这不是惯用法。因此,在这里使用标志是可以的。

3) 这是技术指南中的一个玩具示例。并非所有Oracle内容都像技术规范或API文档一样权威。有些教程存在错误陈述或不完整的情况。可能编写代码的原因是作者认为读者对中断如何工作并不熟悉,最好尽量减少使用。技术撰稿人已知会做出这样的选择。

如果我重写此示例以使用中断,则仍将保留标志; 我将使用中断来终止,并使用标志实现挂起/恢复功能。


当然可以,但这是官方Java文档的一部分,而不是某个公司的代码,所以我认为肯定有充分的理由来做这两件事情。 - Stephen

1
请查看文档。你的线程应该检查thread.isInterrupted()状态,例如:
Thread myThread = new Thread()  {
    @Override
    public void run() {
        while (!this.isInterrupted()) {
            System.out.println("I'm working");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //We would like also exit from the thread
                return;
            }
        }
    }
};

如果你想停止线程,你需要调用

myThread.interrupt();

除此之外,我们还可以使用静态方法Thread.interrupted()来检查状态,但是该方法会在检查后清除状态,需要再次调用myThread.interrupt()来设置状态。但我不建议使用Thread.interrupted()方法。
这种方法可以优雅地停止线程。
因此,我也看不到使用额外的volatile变量的理由。
可以通过像@lexicore答案中的额外volatile标志来实现相同的行为,但我认为这是冗余的代码。

1
这并没有真正回答问题。 - lexicore
如果在这个示例代码中用this.interrupted()替换this.isInterrupted(),我认为没有问题。你不需要重置标志,因为循环将结束并且线程将终止。此外,至少根据https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html,isInterrupted()是用于一个线程查询另一个线程状态的,而不是用于线程查询自身状态的。 - Stephen
@Stephen:在具体的例子中它可以工作,但是一般情况下问题在于它容易出错,不必要地重置标志,人们可能会选择它因为它很容易使用(没有Thread.currentThread()这样的东西),并且对副作用毫不知情。而一个任务检查自己线程的状态使用Thread.currentThread().isInterrupted()是可以的。 - Nathan Hughes
@NathanHughes 好吧,isInterrupted()和interrupted()都不需要Thread.currentThread()的东西。无论如何,我想我会相信你,不必要地重置标志可能是不好的。 - Stephen
@NathanHughes 所以只是为了确保我理解正确,我们需要 isInterrupted() 的情况是因为如果在 catch 块之后的瞬间接收到中断,否则我们可能会使用 while(true) (因为 Thread.sleep() 也会检查是否中断)。虽然如果我们移除对 Thread.sleep() 的调用,它又会变得必要,所以即使这不可能发生,保留它可能是最安全的。 - Stephen
显示剩余6条评论

0

@NathanHughes 是完全正确的,但我将他的长篇回答简化成几个词:第三方代码

这不仅仅是关于“愚蠢的初级开发人员”,在应用程序的生命周期中的某一点,您将使用许多第三方库。这些库不会很绅士地尊重您对并发的假设。有时它们会悄悄地重置中断标志。使用单独的标志变量可以解决这个问题。

不,您不能摆脱第三方代码并结束一天的工作。

假设您正在编写一个库,例如 ThreadPoolExecutor。您的库中的某些代码需要处理中断...或者甚至无条件地重置中断标志。为什么?因为前一个 Runnable 已经完成,新的 Runnable 即将到来。不幸的是,在那个时候可能会存在旧的中断标志,它是针对...等等,它再次针对谁?它可能已经针对前一个 Runnable 或新的(尚未运行的)Runnable - 没有办法确定。这就是为什么您要在 FutureTask 中添加 isCancelled 方法的原因。并在执行新的 Runnable 之前无条件地重置中断标志。听起来耳熟能详吗?

Thread#interrupt 完全与运行在该线程上的实际工作单元分离,因此需要添加其他标志。一旦开始这样做,你必须做到底 - 直到最外层和最内层的工作单元。在效果上,运行未知的用户提供的代码会使 Thread#interrupted 不可靠(即使 Thread#interrupt 仍然运行良好!)


我并不是很理解你的那一段话,开头是“假设你正在编写一个库”。如果我创建一个库,我只需要关注当前线程的代码,而不是“一个新的Runnable [即将到来]”。 - Stephen
@Stephen 如果你从库代码中调用第三方代码(例如插件或回调函数),那么你的库可能无法完全控制线程。如果你调用一个可中断的回调函数,然后又调用了另一个回调函数,如果线程在这些回调函数之间被中断了,你该怎么办?吞掉中断?崩溃线程?如果其中一个回调函数中断了自己(因为它没有其他方法来执行取消操作),该怎么办?回调函数本身不会吞掉它的中断(因为它无法在没有竞争的情况下这样做),所以你的库将不得不处理它。 - user1643723
@Stephen 只是为了澄清一件事情,你曾经在受控的服务器环境之外编写过 Java 代码吗?一旦你编写自己的 ThreadPoolExecutor(是的,Executor 也可以是“库”),或者创建一个在服务器容器之外运行的应用程序,你就必须处理管理顶层工作单元,在这种情况下,让 InterruptedException 冒泡不再是一个选项。在这种情况下,有时您需要重置中断标志而不理解谁设置了它。因此,您将不得不在崩溃线程(不好)和默默地吞下第三方中断之间做出选择。 - user1643723

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