Windows 用户前言: 通常在 Windows 10 上,我从 IntelliJ IDEA、Eclipse 或 Git Bash 运行我的 Java 程序。它们都不会在Ctrl-C 上触发任何 JVM 关闭挂钩,可能是因为它们以比常规 Windows 终端 cmd.exe 更不合作的方式杀死进程。因此,为了测试整个场景,我真的不得不从 cmd.exe 或 PowerShell 运行 Java。
更新:在 IntelliJ IDEA 中,您可以单击看起来像从左到右指向空方格的箭头的“退出”按钮 - 而不是像实心方格一样的“停止”按钮在典型的音频/视频播放器上。另请参阅here 和here 了解更多信息。
请查看Javadoc for Runtime.addShutdownHook(Thread)。它解释了关闭钩子只是一个已初始化但未启动的线程,它将在 JVM 关闭时启动。它还指出您应该以防御性和线程安全的方式对其进行编码,因为不能保证所有其他线程都已中止。
让我告诉你这个效果。因为不幸的是,您没有提供应有的 MCVE,因为代码 sn-ps 不做任何事情来重现您的问题并不是特别有用,所以我创建了一个以解释您的情况似乎发生了什么:
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() 方法添加到Result 并在关闭后调用getter 或setter 时抛出错误来模拟这一点。所以关闭钩子不会终止其他线程或依赖它们被终止,它只是在必要时处理(同步和)关闭资源以提供一致性。
更新:这是Result 类具有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)