Java Shutdown Hook 没有像我预期的那样工作

Java ShutdownHook not working as I expect it to

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

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

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

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

然后我将我的 while 循环直接放在 main 方法中的这个钩子下面 making:

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

但是在这段代码中,关闭挂钩似乎并不是 运行 的最后一件事。终止后,退出代码 运行s 立即执行,然后 while 循环的更多迭代也会执行。

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

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

更新:在 IntelliJ IDEA 中,您可以单击看起来像从左到右指向空方块的箭头的“退出”按钮 - 而不是看起来像的“停止”按钮一个实心方块,就像典型的 audio/video 播放器一样。另请参阅 here and here 了解更多信息。


请看Javadoc for Runtime.addShutdownHook(Thread)。它解释说关闭挂钩只是一个初始化但未启动的线程,它将在 JVM 关闭时启动。它还声明您应该以 thread-safe 方式进行防御性编码,因为不能保证所有其他线程都已中止。

让我给你看看这个效果。因为不幸的是你没有提供你应该提供的 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 class 中,以便拥有一个可变对象,该对象可以声明为 final 并在关闭挂钩线程中使用。它确实由

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

现在如果我们像这样 运行 程序 3x,总是在一秒钟左右后按 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 运行s的时候已经全部关闭了。可能会出现竞争条件。
  • 如果要确保首先打印的内容也是写入结果文件的内容,请在 Result 实例上同步,例如通过 synchronized(result).
  • 了解关闭挂钩的目的是关闭资源,而不是关闭线程。所以你真的需要做到 thread-safe.

如您所见,在这个例子中即使没有 thread-safety 也没有发生任何不好的事情,因为 Result 实例是一个非常简单的对象,我们将它保存在一致的状态。即使我们保存了一个中间结果并且计算会在之后继续进行,也不会造成任何伤害。在接下来的 运行 中,程序将从保存的点开始 re-start 它的工作。

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

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


更新: 这里是变体,其中 Result class 有一个 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)