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 IDEA、Eclipse 或 Git Bash。它们都不会在 Ctrl-C
上触发任何 JVM 关闭挂钩,可能是因为它们以比常规 Windows 终端更不合作的方式终止进程 cmd.exe.因此,为了测试整个场景,我真的必须从 cmd.exe 或 PowerShell[=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
方法有已调整使用它。方法 loadPreviousResult
和 calculateResult
保持不变。请注意关闭挂钩如何使用 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)
我想要做的是运行 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 IDEA、Eclipse 或 Git Bash。它们都不会在 Ctrl-C
上触发任何 JVM 关闭挂钩,可能是因为它们以比常规 Windows 终端更不合作的方式终止进程 cmd.exe.因此,为了测试整个场景,我真的必须从 cmd.exe 或 PowerShell[=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
方法有已调整使用它。方法 loadPreviousResult
和 calculateResult
保持不变。请注意关闭挂钩如何使用 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)