【问题标题】:How do I make sure that a Java thread will never run an infinite loop?如何确保 Java 线程永远不会运行无限循环?
【发布时间】:2020-05-24 03:06:00
【问题描述】:

我正在做一个读取计算机能耗的项目。对于某些背景,能量读数是在 C 中的低级别完成的,但我正在开发一个 Java 应用程序,它使用本机调用来获取读数,然后用它们做一些更高级别的事情。

我现在正在做的是创建一个EnergyReadingCollector 对象,该对象作为线程运行,抓取并存储读数,直到您告诉它停止。您可以在其他一些代码运行时使用它,然后在最后您有一堆关于程序如何消耗能量的数据。制作一些漂亮的图表来打动女士们,你知道;)

当您跟踪的代码部分非常快时,就会出现我的问题。长时间拉伸不会出现问题,但有时我的程序会在快速位上运行无限循环。

这是我的课。我遗漏了很多细节,因为这个问题只与这个程序的线程方面有关。我认为这足以说明发生了什么。

public class EnergyReadingCollector implements Runnable
{
    private ArrayList<double> readings;
    private volatile boolean exit = false;

    public void run()
    {
        readings.clear();
        exit = false;
        while (!exit) {
            double reading = getReading();
            readings.add(reading);
        }
    }

    public void end()
    {
        exit = true;
    }

}

简单用例:

EnergyReadingCollector ec = new EnergyReadingCollector();
new Thread(ec).start();
//code section to track
ec.end();

ec 对象现在有一个内部存储的列表,其中包含在此代码部分中发生的所有能量读数。

我认为会发生无限循环,因为快速代码在线程开始之前就结束了。所以它执行并且ec.end() 发生,但是然后线程启动并等待ec.end() 发生。它没有意识到这艘船已经航行并且永远被困在等待中。

我对为什么会发生这种情况是正确的吗?如果是这样,我该如何防止这种情况发生?是保证被跟踪代码至少运行一定时间的唯一解决方案吗?起初这听起来很合理,因为跟踪几微秒代码的能量消耗是没有意义的。但是,代码可能有一个异常导致它过早中止,所以我宁愿有一些方法来处理这种情况。还可以选择从我的 run() 方法的开头删除 exit = false; 行,但我把它放在那里是为了确保同一个对象可以用于在一个程序中多次不同时间收集能量。

这种情况有线程安全约定吗?

另外,你觉得EnergyReadingCollector这个名字怎么样?这不是我最喜欢的,但它似乎足够具有描述性,我想不出比这更好的了。接受建议:)

编辑: 我做了 cmets 所说的在自己的 reinit() 方法中将 exit 设置为 false 。它实际上更有意义,而且我还有其他可以放入其中的功能。从那以后没有任何问题,所以这可能是问题所在。谢谢!

【问题讨论】:

  • 不要在 run 方法中初始化退出。如果您希望能够重用该实例,请创建一个重置方法,将退出变量重置为 false。
  • @ClaudioCorsi 关于不在run 中初始化exit 是正确的。详情请看我的回答。如果其中一个答案有帮助,请考虑接受。

标签: java multithreading thread-safety


【解决方案1】:

开始阶段创建Runnable并启动线程,这是一个异步操作:

EnergyReadingCollector ec = new EnergyReadingCollector();
                        // Thread A: ec.exit = false;
new Thread(ec).start(); // Thread B: ec.exit = false;
                        // Thread B: while (!ec.exit) {

结束阶段设置标志:

ec.end();               // Thread A: ec.exit = true;

假设能源消耗“部分”中代码的执行时间接近于零,以下事件顺序是可能的(丢失更新),这会导致无限循环:

// EnergyReadingCollector ec = new EnergyReadingCollector();
Thread A: ec.exit = false;
// new Thread(ec).start();
Thread A: ec.exit = true;
Thread B: ec.exit = false;
Thread B: while (!ec.exit) {

要解决这个问题,您需要做的就是删除run() 中的初始化,因为exit 有一个初始化程序,所以没有必要。

请注意,您应该始终通过Thread.join() 等待线程完成。否则,即使最后一个线程仍在运行,也可能会在后续能耗部分启动新线程,这可能会导致达到资源限制。

回到exit 的初始化。如果Runnable 实例应该是可重用的,您可以在离开run() 之前重置exit。无论如何,在重用实例之前,您必须确保线程不再运行。 Thread.join() 会这样做。

【讨论】:

    【解决方案2】:

    我认为会发生无限循环,因为快速代码在线程开始之前就结束了。所以它执行并且 ec.end() 发生,但随后线程启动并等待 ec.end() 发生。它没有意识到这艘船已经航行并且永远被困在等待中。

    我不这样。让我们假设这是真的。我是那种情况,

            while (!exit) {
              double reading = getReading();
              readings.add(reading);
            }
    

    while 循环根本不会运行,因为 exit 为真。

    我可以看到 3 种可能导致线程未达到退出条件的情况:

    • getReading() 调用永远不会完成。这可以通过在下一行添加断点(或打印语句,如果未设置调试环境)来检查。
    • start()end() 之间的代码永远不会完成。
    • start()end() 之间的代码引发了一个异常,该异常被捕获在更大的范围内,该范围未包含在上面的示例代码中。

    您能否测试这些可能性并发布您的发现?

    编辑:

    我认为EnergyReadingCollector 这个名字很好。如果您不喜欢前者,可以将其更改为 EnergyLevelCollector

    【讨论】:

      【解决方案3】:

      问题在于exit 不是易失性的。因此,在将值设置为 true 和读取值之间的关系之前没有发生任何事情。

      JIT 可以决定将值的读数提升到循环之外:

      if(exit) return;
      while(true){
         .. stuff
      }
      

      【讨论】:

      • 它在我发布的代码中说exit 是易变的
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-19
      • 2016-10-12
      • 1970-01-01
      • 2019-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多