如何处理:在循环中调用'Thread.sleep()',可能会导致忙等待

22

大家好,如何处理这样的代码和警告?

private void listenOnLogForResult() {
    String logs = "";
    int timeCounter = 1;
    while (logs.isEmpty()) {
        try {
            timeCounter++;
            Thread.sleep(2000); // Wait 2 seconds
        } catch (InterruptedException e) {
            log.error(e.getLocalizedMessage(), e);
        }
        if (timeCounter < 30) {
            logs = checkLogs()
        } else {
            logs = "Time out";
        }
    }
}

我需要暂停当前线程2秒钟以等待文件被填充,但是我的Intelij出了问题。

输入图像描述

并且我从Sonar那里得到错误消息:

SonarLint:要么重新中断此方法,要么重新抛出“InterruptedException”。

我已经尝试过许多ExecutorService,但它总是在单独的线程中运行,而我需要暂停当前线程。

请帮帮我..


1
也许这会有所帮助 https://dev59.com/VlQJ5IYBdhLWcg3wMCx_。对于您的异常,您只是记录它,而while循环继续。我猜这是一个无限循环的潜在候选者。 - aksappy
2个回答

28

繁忙等待的警告

这是来自IntelliJ的一个可疑警告,意思是你所做的事情通常是必需的。换句话说,它检测到了一种被过度使用的模式,但是它的使用不能减少到0。所以,最好的解决方案可能就是告诉IntelliJ在这里闭嘴。

它关注的问题不是Thread.sleep。那不是问题所在。然而,IntelliJ的检测器需要它来找到这种情况,但它抱怨的不是这个,这可能有点难以理解。

IntelliJ担心的是,你在不断地浪费循环检查log.isEmpty()而没有理由。它对这段代码的while部分有问题,而不是sleep。它更希望看到的是你调用某种logs.poll()方法,该方法会等待直到有新的日志出现才主动唤醒。

如果这一切都在一个单独的Java进程中运行,那么您确实可以重写整个系统(包括重写这里的任何“log”以及完全重新构想“checkLogs()”方法:不是出去检查,而是需要唤醒生成日志的代码)。
如果不是这样,很可能您需要告诉IntelliJ关闭它:如果没有完全重新设计系统,您所做的是不可避免的。
重新中断警告
您在这里的异常处理是令人遗憾的。
您的异常处理总体而言
不要编写只记录一些内容然后继续执行的“catch”块。这是非常糟糕的错误处理方式:系统的变量和字段现在处于未知状态(您刚刚捕获并记录了一些内容:肯定意味着您不知道发生了什么条件导致执行这行代码!),但代码将继续执行。很有可能,“捕获异常并继续执行”的代码风格会导致更多的异常:通常,对未知状态进行操作的代码会很快崩溃。
然后,如果那个崩溃被以同样的方式处理(捕获它,记录它,继续执行),那么你会遇到另一个崩溃。最终,你的代码会在遇到问题时打印186个异常到日志中,而除了第一个异常之外,其他都毫无意义。这是很糟糕的做法。
你还让调用代码完全无法恢复。异常的目的是要无限向上冒泡:要么异常被真正知道如何处理问题的代码捕获(而仅仅记录异常并不是处理异常!),这是你所做的无法实现的,要么异常应该一直冒泡到入口点处理程序,这是记录错误和中止入口点处理程序的正确位置。
入口点处理程序是一个通用的模块或应用程序运行器;开箱即用的,内置在java.exe中的代码最终会调用你的psv main()方法,这是最明显的“入口点运行器”,但还有更多:Web框架最终会调用你的一些代码来处理Web请求:你的代码类似于psv main():它是入口点,而调用它的Web框架中的代码则是入口点运行器。
入口点运行器有充分的理由去捕获(Throwable t),并在catch块中主要记录日志,尽管它们通常应该记录更多的内容,而不仅仅是异常(例如,Web处理程序应该记录请求的详细信息,比如发送了哪些HTTP参数,请求的路径是什么,可能还有头部等)。然而,任何其他代码都不应该这样做。
如果你不知道该怎么做,也不想考虑异常可能意味着什么,正确的“随便了,只要编译通过javac”代码策略是将异常类型添加到你的throws行中。如果这不可行,catch块中的正确代码是:
} catch (ExceptionIDoNotWantToThinkAboutRightNow e) {
    throw new RuntimeException("Uncaught", e);
}

这将确保代码不会只是愉快地继续运行在未知状态上,并确保您在日志中获得完整的详细信息,并确保调用代码可以捕获并处理它(如果可能的话),并确保任何自定义的日志信息(如HTTP请求详细信息)有机会记录到日志中。四赢。
特别是这种情况:InterruptedEx是什么意思?
当在Java进程中运行的某些代码调用yourThread.interrupt()时,就会发生InterruptedException,而且没有其他方式可以发生InterruptedException。如果用户按下CTRL+C,或者进入任务管理器并点击“结束进程”,或者如果您的Android手机决定是时候让您的应用退出,因为内存需要用于其他用途-没有这些情况可能导致InterruptedException。您的线程只会被Java在中间步骤中终止(如果您想在关闭时采取措施,请使用Runtime.getRuntime().addShutdownHook)。唯一的方法是某些代码调用.interrupt(),核心库中没有任何内容会这样做。因此,InterruptedException意味着您认为“在此线程上调用.interrupt()”。这取决于您。
最常见的定义是“我请求你停止”:友好地关闭线程。通常情况下,如果你想退出整个虚拟机(只需调用System.shutdown - 你已经需要处理用户按下CTRL+C的情况,为什么要以不同的方式编写两次关闭代码?),友好地关闭线程是不好的 - 但有时你只想让一个线程停止。因此,通常在catch (InterruptedException e)块中放入的最佳代码只是return;,什么都不做。不要记录任何东西:中断是有意的:是你自己写的。很可能在你的代码库中根本找不到这个InterruptedException,所以它永远不会发生。
在你的具体代码中,如果你的代码决定停止记录器线程,那么记录器线程将向错误日志中记录一些内容,然后将其2秒的等待时间缩短为立即检查日志,然后继续执行。这听起来完全没有用处。
但是,它的意思取决于你想要的。如果你想要一个能让用户立即点击“强制检查日志”按钮的功能,那么你可以定义中断日志线程只是为了节省2秒的时间(但是请确保有一个空的catch块,并在注释中解释这是你的设计方式,显然不要记录它)。如果你还想要一个“停止日志线程”的按钮,可以使用AtomicBoolean来跟踪“运行”状态:当点击“停止日志刷新”按钮时,将AB设置为“false”,然后中断线程:然后你粘贴的代码需要检查AB并且return;以关闭线程,如果AB为false

我觉得很奇怪的是,你在这里对异常处理方法非常激烈地批评(使用了像“可耻”和大量粗体字等词语),然后在最后一段中承认“你可以定义中断记录线程只是快捷方式2秒钟”。我认为这正是OP试图定义它的方式,我也经常使用这种模式。实际上,除非您正在构建具有中断机制的复杂多线程应用程序,否则通常中断的意思就是“嘿,SomeThread,停止睡觉并继续工作”。 - Adam Burley
@AdamBurley,这显然是一个错误的命题。如果OP的意图是这样做,他们不会在ERROR级别发送日志消息,这是显而易见的。你的次要想法是中断通常意味着“停止等待并立即检查,但否则继续执行你的任务”,这也只是你编造的东西。这样做没问题,但它不是“正确的选择”甚至不是“最常见的选择”。这是一种选择,和其他许多选择一样好。 - rzwitserloot
关于Thread.currentThread().interrupt();怎么看?我以为在捕获InterruptedException时这是一种常见的做法,但是我注意到你没有提到它。我很好奇为什么。提前谢谢。 - Nom1fan
中断一个线程会“抬起旗帜”。第一个“处理”中断的操作会将旗帜降下来;Thread.interrupted()被认为是“处理中断”的方式(如果旗帜已经升起,则返回true并将旗帜降下来),因为假设在该if块中的内容会处理中断。抛出InterruptedEx也被视为处理中断的方式。应该这样做:毕竟,如果你抛出IntEx但保持旗帜升起,那么处理intex的代码将会得到一个非常不愉快的惊喜!它可能会执行一些无害的操作,比如“记录一些东西”,但由于I/O被中断而导致记录文件发送失败。 - rzwitserloot
那么你应该升起旗帜吗?常见的观点完全错误:很可能不应该. “只是重新升起它,这样更好”这个想法非常有害,因为它会破坏任何处理情况的尝试。只有在特定情况下,你才应该重新升起旗帜——如果你并没有真正处理它,而只是插入一些事情然后让中断状态传播。这取决于具体情况。但通常来说,不应该重新升起旗帜,如果必须制定一个笼统的规则,“不要重新升起它”比“重新升起它”要好得多。 - rzwitserloot
我在这个回答中要传达的具体观点是:打断意味着你说的那样(因为除非你在代码中写".interrupt()",否则它们不会发生),并且没有明确的重要约定。因此,[1] 如果你不使用该系统,请不要试图处理无法发生的未指定事物,[2] 如果你使用它,请决定其含义并相应行动。对于不知道会发生什么的库而言,请保持简单和一致;你唯一真正的方法是抛出InterruptedEx或其他异常,并保持该标志下降。 - rzwitserloot

-13
fun sleep(timeMillis: Long) {
    val currentTimeMillis = System.currentTimeMillis()
    while (true) {
        if (System.currentTimeMillis() - currentTimeMillis >= timeMillis) {
            break
        }
    }
}

使用这个方法(它是由Kotlin编写的,你需要将其转换为Java)


13
这实际上是忙等待,比Thread.sleep还糟糕。而且Kotlin并不是Java。 - slindenau

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