首页 > 解决方案 > Java ShutdownHook 没有像我预期的那样工作

问题描述

我想要做的是在 while(true) 循环中运行一些代码,然后当我点击 IntelliJ 或控制 c 中的终止按钮时,第二个代码块运行干净地终止并将我的所有进度保存到一个文件. 我目前的程序使用在我的 main 方法中运行的代码工作:

File terminate = new File(terminatePath);
while(!terminate.canRead()) {
    // process
}
// exit code

但是,为了终止代码,我必须在目录“terminatePath”中创建一个文件,当我想再次开始运行时,我必须删除该文件。这是非常草率和烦人的,所以我想学习正确的方法来做这样的事情。我在网上发现的大多数情况都说要使用关闭挂钩并在下面提供以下代码:

Runtime.getRuntime().addShutdownHook(new Thread() {
    public void run() { 
        // exit code
    }
});

我将我的while循环直接放在主要方法制作中的这个钩子下面:

public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread() {
        public void run() { 
           // exit code
        }
    });
    while (true) {
    // process
    }
}

然而在这段代码中,关闭钩子似乎并不是最后运行的东西。终止后,退出代码会立即运行,然后还会执行几次 while 循环的迭代。

我假设我错误地应用了退出挂钩,但我似乎无法在网上找到正确的方法。我可以对此代码进行哪些更改以使 while 循环在运行退出挂钩之前可靠地停止?谢谢。

标签: javaintellij-ideawhile-loopruntimeterminate

解决方案


Windows 用户前言:通常在 Windows 10 上,我从IntelliJ IDEAEclipseGit Bash运行我的 Java 程序。它们都不会触发任何 JVM 关闭挂钩,可能是因为它们以比常规 Windows 终端cmd.exeCtrl-C更不合作的方式杀死进程。因此,为了测试整个场景,我真的不得不从cmd.exe或从PowerShell运行 Java 。

更新:在IntelliJ IDEA中,您可以单击看起来像从左到右指向空方格的箭头的“退出”按钮 - 而不是像典型音频/视频播放器上那样看起来像实心方格的“停止”按钮。另请参阅此处此处了解更多信息。


请查看Javadoc 的Runtime.addShutdownHook(Thread). 它解释了关闭钩子只是一个已初始化但未启动的线程,它将在 JVM 关闭时启动。它还指出您应该以防御性和线程安全的方式对其进行编码,因为不能保证所有其他线程都已中止。

让我给你看一下这个效果。因为不幸的是,您没有提供应有的MCVE,因为代码片段对重现您的问题没有特别帮助,所以我创建了一个以解释您的情况似乎发生了什么:

public class Result {
  private long value = 0;

  public long getValue() {
    return value;
  }

  public void setValue(long value) {
    this.value = value;
  }

  @Override
  public String toString() {
    return "Result{value=" + value + '}';
  }
}
import java.io.*;

public class ResultShutdownHookDemo {
  private static final File resultFile = new File("result.txt");
  private static final Result result = new Result();
  private static final Result oldResult = new Result();

  public static void main(String[] args) throws InterruptedException {
    loadPreviousResult();
    saveResultOnExit();
    calculateResult();
  }

  private static void loadPreviousResult() {
    try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(resultFile)))) {
      result.setValue(Long.parseLong(bufferedReader.readLine()));
      oldResult.setValue(result.getValue());
      System.out.println("Starting with intermediate result " + result);
    }
    catch (IOException e) {
      System.err.println("Cannot read result, starting from scratch");
    }
  }

  private static void saveResultOnExit() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      System.out.println("Shutting down after progress from " + oldResult + " to " + result);
      try { Thread.sleep(500); }
      catch (InterruptedException ignored) {}
      try (PrintStream out = new PrintStream(new FileOutputStream(resultFile))) {
        out.println(result.getValue());
      }
      catch (IOException e) {
        System.err.println("Cannot write result");
      }
    }));
  }

  private static void calculateResult() throws InterruptedException {
    while (true) {
      result.setValue(result.getValue() + 1);
      System.out.println("Running, current result value is " + result);
      Thread.sleep(100);
    }
  }

}

这段代码所做的只是简单地增加一个数字,包装到一个Result类中,以便拥有一个可变对象,该对象可以声明为 final 并在关闭挂钩线程中使用。它确实通过

  • 如果可能,从先前运行保存的文件中加载中间结果(否则从 0 开始计数),
  • 每 100 毫秒递增一次值,
  • 在 JVM 关闭期间将当前中间结果写入文件(人为地将关闭挂钩减慢 500 毫秒以证明您的问题)。

现在如果我们像这样运行程序 3 次,总是在大约Ctrl-C一秒钟后按下,输出将是这样的:

my-path> del result.txt

my-path> java -cp bin ResultShutdownHookDemo
Cannot read result, starting from scratch
Running, current result value is Result{value=1}
Running, current result value is Result{value=2}
Running, current result value is Result{value=3}
Running, current result value is Result{value=4}
Running, current result value is Result{value=5}
Running, current result value is Result{value=6}
Running, current result value is Result{value=7}
Shutting down after progress from Result{value=0} to Result{value=7}
Running, current result value is Result{value=8}
Running, current result value is Result{value=9}
Running, current result value is Result{value=10}
Running, current result value is Result{value=11}
Running, current result value is Result{value=12}

my-path> java -cp bin ResultShutdownHookDemo
Starting with intermediate result Result{value=12}
Running, current result value is Result{value=13}
Running, current result value is Result{value=14}
Running, current result value is Result{value=15}
Running, current result value is Result{value=16}
Running, current result value is Result{value=17}
Shutting down after progress from Result{value=12} to Result{value=17}
Running, current result value is Result{value=18}
Running, current result value is Result{value=19}
Running, current result value is Result{value=20}
Running, current result value is Result{value=21}
Running, current result value is Result{value=22}

my-path> java -cp bin ResultShutdownHookDemo
Starting with intermediate result Result{value=22}
Running, current result value is Result{value=23}
Running, current result value is Result{value=24}
Running, current result value is Result{value=25}
Running, current result value is Result{value=26}
Running, current result value is Result{value=27}
Running, current result value is Result{value=28}
Running, current result value is Result{value=29}
Running, current result value is Result{value=30}
Shutting down after progress from Result{value=22} to Result{value=30}
Running, current result value is Result{value=31}
Running, current result value is Result{value=32}
Running, current result value is Result{value=33}
Running, current result value is Result{value=34}
Running, current result value is Result{value=35}

我们看到以下效果:

  • 事实上,在关闭钩子启动后,主线程会继续运行一段时间。
  • 在第 2 次和第 3 次运行中,程序继续运行主线程最后打印到控制台的值,而不是等待 500 毫秒之前关闭钩子线程打印的值。

得到教训:

  • 不要相信正常线程在shutdown hook运行时已经全部关闭。可能会出现竞争条件。
  • 如果您想确保首先打印的内容也是写入结果文件的内容,请在Result实例上同步,例如通过synchronized(result).
  • 了解关闭挂钩的目的是关闭资源,而不是关闭线程。所以你真的需要让它成为线程安全的。

正如你所看到的,在这个例子中,即使没有线程安全,也没有发生任何不好的事情,因为Result实例是一个非常简单的对象,我们将它保存在一个一致的状态中。即使我们保存了一个中间结果并且之后会继续计算,也不会造成任何伤害。在下一次运行中,程序只会从保存的点重新开始工作。

唯一丢失的工作是关闭挂钩保存结果后所做的工作,这应该不是问题,只要不影响文件或数据库等其他外部资源。

如果是后者,您需要确保在保存中间结果之前关闭这些资源。这可能会导致主应用程序线程中的错误,但可以避免不一致。close()您可以通过在关闭后调用 getter 或 setter 时添加方法Result并引发错误来模拟这一点。所以关闭钩子不会终止其他线程或依赖它们被终止,它只是根据需要处理(同步和)关闭资源以提供一致性。


更新:这是Result该类具有close方法并且该saveResultOnExit方法已被调整以使用它的变体。方法loadPreviousResultcalculateResult保持不变。请注意synchronized在将中间结果复制到另一个变量后,关闭挂钩如何使用和关闭资源。如果您想在将同步Result写入文件之前保持打开状态,则复制并不是绝对必要的。但是,在这种情况下,您需要确保内部结果状态不能被另一个线程以任何方式更改,即资源封装很重要。

public class Result {
  private long value = 0;
  private boolean closed = false;

  public long getValue() {
    if (closed)
      throw new RuntimeException("resource closed");
    return value;
  }

  public void setValue(long value) {
    if (closed)
      throw new RuntimeException("resource closed");
    this.value = value;
  }

  public void close() {
    closed = true;
  }

  @Override
  public String toString() {
    return "Result{value=" + value + '}';
  }
}
import java.io.*;

public class ResultShutdownHookDemo {
  private static final File resultFile = new File("result.txt");
  private static final Result result = new Result();
  private static final Result oldResult = new Result();

  public static void main(String[] args) throws InterruptedException {
    loadPreviousResult();
    saveResultOnExit();
    calculateResult();
  }

  private static void loadPreviousResult() {
    try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(resultFile)))) {
      result.setValue(Long.parseLong(bufferedReader.readLine()));
      oldResult.setValue(result.getValue());
      System.out.println("Starting with intermediate result " + result);
    }
    catch (IOException e) {
      System.err.println("Cannot read result, starting from scratch");
    }
  }

  private static void saveResultOnExit() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      long resultToBeSaved;
      synchronized (result) {
        System.out.println("Shutting down after progress from " + oldResult + " to " + result);
        resultToBeSaved = result.getValue();
        result.close();
      }
      try { Thread.sleep(500); }
      catch (InterruptedException ignored) {}
      try (PrintStream out = new PrintStream(new FileOutputStream(resultFile))) {
        out.println(resultToBeSaved);
      }
      catch (IOException e) {
        System.err.println("Cannot write result");
      }
    }));
  }

  private static void calculateResult() throws InterruptedException {
    while (true) {
      result.setValue(result.getValue() + 1);
      System.out.println("Running, current result value is " + result);
      Thread.sleep(100);
    }
  }

}

现在您可以在控制台上看到来自主线程的异常,因为它会在关闭挂钩已经关闭资源后尝试继续工作。但这在关闭期间无关紧要,并确保我们确切知道在关闭期间写入输出文件的内容,并且在此期间没有其他线程修改要写入的对象。

my-path> del result.txt

my-path> java -cp bin ResultShutdownHookDemo
Cannot read result, starting from scratch
Running, current result value is Result{value=1}
Running, current result value is Result{value=2}
Running, current result value is Result{value=3}
Running, current result value is Result{value=4}
Running, current result value is Result{value=5}
Running, current result value is Result{value=6}
Running, current result value is Result{value=7}
Shutting down after progress from Result{value=0} to Result{value=7}
Exception in thread "main" java.lang.RuntimeException: resource closed
        at Result.getValue(Result.java:7)
        at ResultShutdownHookDemo.calculateResult(ResultShutdownHookDemo.java:51)
        at ResultShutdownHookDemo.main(ResultShutdownHookDemo.java:11)

my-path> java -cp bin ResultShutdownHookDemo
Starting with intermediate result Result{value=7}
Running, current result value is Result{value=8}
Running, current result value is Result{value=9}
Running, current result value is Result{value=10}
Running, current result value is Result{value=11}
Running, current result value is Result{value=12}
Shutting down after progress from Result{value=7} to Result{value=12}
Exception in thread "main" java.lang.RuntimeException: resource closed
        at Result.getValue(Result.java:7)
        at ResultShutdownHookDemo.calculateResult(ResultShutdownHookDemo.java:51)
        at ResultShutdownHookDemo.main(ResultShutdownHookDemo.java:11)

my-path> java -cp bin ResultShutdownHookDemo
Starting with intermediate result Result{value=12}
Running, current result value is Result{value=13}
Running, current result value is Result{value=14}
Running, current result value is Result{value=15}
Running, current result value is Result{value=16}
Running, current result value is Result{value=17}
Shutting down after progress from Result{value=12} to Result{value=17}
Exception in thread "main" java.lang.RuntimeException: resource closed
        at Result.getValue(Result.java:7)
        at ResultShutdownHookDemo.calculateResult(ResultShutdownHookDemo.java:51)
        at ResultShutdownHookDemo.main(ResultShutdownHookDemo.java:11)

推荐阅读