Thread.join使用“重试”模式

4

当我查看使用Thread的示例代码时,经常会遇到这种重试模式。

boolean retry = true;
while (retry) {
    try {
        myThread.join();
        retry = false;
    } catch (InterruptedException e) {
        // Nothing ...
    }
}

join() 是用来永久等待的。

如果在调用 join 方法前或期间,当前线程被中断并收到 InterruptedException 异常,那么 myThread 真正加入了吗?

这是一些剪切粘贴的残留物变成的模式吗?


我建议您迁移到 Reactive Extensions https://github.com/ReactiveX/RxJava/wiki/Combining-Observables - MLProgrammer-CiM
2个回答

2
另一个ThreadInterruptedException的情况下没有加入(join)你的Threadjoin()应该永远等待或者直到调用Thread被中断。
首先,您需要考虑是否实际上会发生InterruptedException。有几种处理它的可能性。您可以:
  • 吞下它。警告:只有在您确定InterruptedException永远不会发生时才这样做。(你确定吗?真的吗?)
  • 再次设置中断标志。如果您的代码不知道如何处理它,但线程还在做其他事情,也许对InterruptedException感兴趣,那么就这样做。
  • 传播它。如果当前方法不知道如何处理它,但是可能调用者知道,则这是最佳解决方案。
  • 处理它。也许您当前的Thread只是想在接收到InterruptedException时停止(通过完成其run()方法)。
  • 将其包装在另一个异常中。有两个变体。
    • 如果InterruptedException本来不应该发生并且明显是错误,则可以将其包装在断言中。
    • 如果您的方法绑定到不允许InterruptedException的特定合同,则可以将其包装在另一个异常中。
  • 无论哪种情况,我强烈建议以某种方式记录InterruptedException。如果您没有记录它,并且它发生了,程序的行为可能很难理解。
    吞下它
    警告:只有在您绝对确定这样做没问题的情况下才能吞下它。你已经被警告过了。
    public static void joinThread(final Thread thread) {
        while (true)
            try {
                thread.join();
                return;
            } catch (final InterruptedException e) {
                Logger.getGlobal().log(Level.ERROR, "Unexpected InterruptedException", e);
            }
    }
    

    作为这个问题的一个变体,您可以选择忽略它而不进行重试。
    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.ERROR, "Unexpected InterruptedException", e);
        }
    }
    

    再次设置interrupted标志。

    如果Thread还在做其他事情,而您的代码不知道如何处理InterruptedException,但其他代码可以处理,则需要执行此操作。

    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.INFO, "Unexpected InterruptedException, resetting flag", e);
            Thread.currentThread().interrupt();
        }
    }
    
    < p > < em > 但是请注意! 这只有在其他代码真正知道该做什么时才是一个好主意。如果其他代码执行相同的操作,并且该操作在循环中(这通常是Thread的情况),那么您刚刚将好的阻塞代码转换为忙等待垃圾,因为interrupted标志永远不会被正确清除。虽然这种解决方案在许多博客文章中被宣传为最佳或真正的解决方案,但只要没有任何能够处理InterruptedException并实际保持标志已清除而不是重置它的代码,就是无意义的。

    传播它

    这是最佳解决方案,如果该方法本身不知道如何处理它,但调用者可能知道。

    public static void joinThread(final Thread thread) throws InterruptedException {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.FINE, "Got InterruptedException, propagating it to the caller", e);
            throw e;
        }
    }
    

    如果调用者对其进行了良好的处理,您可以删除不必要的部分,如日志记录和重新抛出。

    处理它。

    如果方法本身知道在被中断时必须执行某些操作,它可以简单地捕获异常并执行预期的操作。

    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.FINE, "Got InterruptedException, handling it", e);
            // ...whatever...
        }
    }
    

    总结

    如果异常不应该发生,因为应用程序不支持中断,并且在第一次调用时不应调用Thread.interrupt()ThreadGroup.interrupt(),则将异常转换为AssertionError是正确的方式。 有两种可能性实现这个目的。

    // Only throws AssertionError if assertions are enabled.
    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.ERROR, "Unexpected InterruptedException", e);
            assert false : e;
        }
    }
    
    // Always throws AssertionError.
    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.FATAL, "Unexpected InterruptedException", e);
            throw new AssertionError(e, "Unexpected Thread interruption.");
        }
    }
    

    如果需要传播InterruptedException但调用者不支持该异常并且无法更改,您可以将其包装成受支持的异常。是否有效并且是否是一个好主意,这取决于调用者。

    public static void joinThread(final Thread thread) {
        try {
            thread.join();
        } catch (final InterruptedException e) {
            Logger.getGlobal().log(Level.ERROR, "Unexpected InterruptedException", e);
            throw new SomeException(e, "Unexpected Thread interruption.");
        }
    }
    

    最后的注意事项

    有些人认为始终不应该使用“吞掉(swallowing)”操作,这是不正确的。如果你有100行代码并且在某个地方使用Thread.sleep()操作有一些好的理由,那么吞掉异常通常是可以接受的。像往常一样,这取决于具体情况。最终,Thread.interrupt()的语义与SIGHUP并没有太大差别,而且SIGHUP通常被忽略。

    也有一些人讨论将其转换为AssertionError是否明智。 AssertionError是一个未经检查的Throwable,甚至不是一个Exception,这意味着编译器不强制代码处理它,并且大多数代码也没有写来处理它 - 这是有意的,因为通过使用AssertionError,我们表明出现了一种错误,这种错误是如此意外以至于代码不知道如何处理它,因此希望停止运行。无论AssertionError是否终止VM取决于哪些线程会由于AssertionError被抛到受影响的ThreadUncaughtExceptionHandler中而被关闭。


    1
    我不知道有任何合法的情况可以忽略 InterruptedException。支持这种想法的答案应该明确说明为什么要忽略 InterruptedException,因为在Java中,这被认为是一个臭名昭著的反模式。 - Marko Topolnik
    1
    将其重新抛出为非检查的可抛出对象,例如 AssertionError,与忽略它几乎一样糟糕。 - aioobe
    @aioobe 不仅仅是未经检查的异常:甚至不是Exception的子类型。这听起来确实像是一种极端措施,有可能导致整个JVM关闭。 - Marko Topolnik
    虽然从学术角度来看,对这两种方法的反对是正确的,但触发 InterruptedException 的唯一方法是调用 Thread.interrupt()ThreadGroup.interrupt()。如果在应用程序中不会发生这种情况,那么为什么要担心它呢?但是您的反对意见对于更通用的答案是正确的,我会进行更新。 - Christian Hujer
    如果我是一个进程并且收到SIGHUP信号,我要么忽略它,要么我会准确地知道该做什么 - 我在收到SIGHUP信号时定义的操作。我们仍然在谈论预编程代码和预编程决策。 - Christian Hujer
    显示剩余4条评论

    1
    如果myThread.join()调用被中断,线程不会被加入。 myThread将继续运行,不知道调用加入的线程已经被中断的事实。
    我自己没有看到过这种模式,我认为这是一个可怕的模式。您不应该以这种方式忽略InterruptedException。请参见this post,了解如何正确处理InterruptedExceptions的描述。

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