Java关机钩子(ShutdownHook)不按我预期工作

huangapple go评论63阅读模式
英文:

Java ShutdownHook not working as I expect it to

问题

以下是您要求的翻译内容:

我所尝试的是在一个 while(true) 循环中运行一些代码,然后当我在 IntelliJ 中点击终止按钮或使用 Ctrl+C 终止时,会运行第二个代码块,它会干净地终止并将所有进展保存到文件中。我目前通过在主方法中运行以下代码使程序正常工作:

File terminate = new File(terminatePath);
while(!terminate.canRead()) {
    // 进程处理
}
// 退出代码

然而,为了终止代码,我必须在 "terminatePath" 目录中创建一个文件,当我想要重新开始运行时,我必须删除那个文件。这样做非常混乱和繁琐,因此我想学习正确的方法来做类似的事情。我在大多数情况下在网上找到的建议是使用关机钩子(shutdown hook),并提供以下代码:

Runtime.getRuntime().addShutdownHook(new Thread() {
    public void run() { 
        // 退出代码
    }
});

我将我的 while 循环直接放在主方法中的这个钩子下面,变成了:

public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread() {
        public void run() { 
           // 退出代码
        }
    });
    while (true) {
        // 进程处理
    }
}

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

我猜想我错误地应用了关机钩子,但我似乎找不到正确的方法。我应该如何更改这段代码,以确保在运行退出钩子之前可靠地停止 while 循环?谢谢。

英文:

What I'm trying to do is run some code in a while(true) loop, then when I hit the terminate button in IntelliJ or control c, a second block of code runs that cleanly terminates and saves all of my progress to a file. I currently have the program working using this code that runs in my main method:

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

However in order to terminate code I have to create a file at the directory "terminatePath" and when I want to start running again I have to delete that file. This is very sloppy and annoying to do, so I'd like to learn the correct method to do something like this. Most cases I've found online say to use a shutdown hook and provide this code below:

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

And I put my while loop directly underneath this hook in the main method making:

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

However in this code, the shutdown hook doesn't seem to be the last thing that runs. Upon terminating, the exit code runs immediately, and then a few more iterations of the while loop execute as well.

I'm assuming that I applied the exit hook incorrectly, but I can't seem to find the correct method online. What can I change to this code to make the while loop reliably stop before running the exit hook? Thanks.

答案1

得分: 1

Windows用户前言: 通常在Windows 10上,我会从_IntelliJ IDEA_、_Eclipse_或_Git Bash_运行我的Java程序。它们都不会在Ctrl-C按下时触发任何JVM关闭钩子,可能是因为它们以比常规的Windows终端_cmd.exe_更不合作的方式终止进程。因此,为了测试这个整个场景,我确实必须从_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次,每次在大约1秒后按下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
<details>
<summary>英文:</summary>
**Preface for Windows users:** Usually on Windows 10 I run my Java programs either from _IntelliJ IDEA_, _Eclipse_ or _Git Bash_. All of them do not trigger any JVM shutdown hooks upon `Ctrl-C`, probably because they kill the process in a less cooperative way than the regular Windows terminal _cmd.exe_. So in order to test this whole scenario I really had to run Java from _cmd.exe_ or from _PowerShell_.
Update: In _IntelliJ IDEA_ you can click the &quot;Exit&quot; button looking like an arrow from left to right pointing into an empty square - not the &quot;Stop&quot; button looking like a solid square just like on typical audio/video player. See also [here](https://youtrack.jetbrains.com/issue/IDEA-253795#focus=Comments-27-4469873.0-0) and [here](https://www.jetbrains.com/help/idea/2020.2/run-tool-window.html#run-toolbar) for more information.
---
Please look at the [Javadoc for `Runtime.addShutdownHook(Thread)`](https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#addShutdownHook-java.lang.Thread-). It explains that the shutdown hook is just an initialised but unstarted thread which will be started when the JVM shuts down. It also states that you should code it defensively and in a thread-safe manner, because there is no guarantee that all other threads have been aborted yet.
Let me show you the effect of this. Because unfortunately you provided no [MCVE](https://stackoverflow.com/help/mcve) as you should have because code snippets doing nothing to reproduce your problem are not particularly helpful, I created one in order to explain what seems to be happening in your situation:
```java
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 &quot;Result{value=&quot; + value + &#39;}&#39;;
}
}
import java.io.*;

public class ResultShutdownHookDemo {
  private static final File resultFile = new File(&quot;result.txt&quot;);
  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(&quot;Starting with intermediate result &quot; + result);
    }
    catch (IOException e) {
      System.err.println(&quot;Cannot read result, starting from scratch&quot;);
    }
  }

  private static void saveResultOnExit() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -&gt; {
      System.out.println(&quot;Shutting down after progress from &quot; + oldResult + &quot; to &quot; + 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(&quot;Cannot write result&quot;);
      }
    }));
  }

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

}

What this code does is to simply increment a number, wrapped into a Result class so as to have a mutable object which can be declared final and used in the shutdown hook thread. It does do by

  • loading an intermediate result from a file which was saved by a previous run, if possible (otherwise starting counting from 0),
  • incrementing the value every 100 ms,
  • writing the current intermediate result to a file during JVM shutdown (artificially slowing down the shutdown hook by 500 ms in order to demonstrate your problem).

Now if we run the program 3x like this, always pressing Ctrl-C after a second or so, the output will be something like this:

my-path&gt; del result.txt
my-path&gt; 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&gt; 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&gt; 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}

We see the following effects:

  • In fact the main thread is continuing to run for a while after the shutdown hook has been started.
  • In the 2nd and 3rd run the program continues to run with the value last printed to the console by the main thread, not with the value printed by the shutdown hook thread before it waited for 500 ms.

Lessons learned:

  • Do not believe the normal threads have all been shut down already when the shutdown hook runs. Race conditions can occur.
  • If you want to make sure that what is printed first is also what is written to the result file, synchronise on the Result instance, e.g. by synchronized(result).
  • Understand that the purpose of a shutdown hook is to close resources, not to close threads. So you really need to make it thread-safe.

As you can see, in this example even without thread-safety nothing bad happened because the Result instance is a very simple object and we saved it in a consistent state. No harm would have been done even if we saved an intermediate result and the calculation would have continued afterwards. In the next run the program just would re-start its work from the point saved.

The only work lost would be the work done after the shutdown hook saved the results, which should not be a problem, as long as no other external resources like files or databases are affected.

If the latter was the case, you would need to make sure that those resources would be closed by the shutdown hook before saving an intermediate result. This might result in errors in the main application thread(s), but avoid inconsistencies. You can simulate this by adding a close() method to Result and throwing an error when calling the getter or setter after it was closed. So the shutdown hook does not terminate other threads or rely on them being terminated, it just takes care of (synchronising on and) closing resources as necessary in order to provide for consistency.


Update: Here is the variant where the Result class has a close method and the saveResultOnExit method has been adjusted to use it. Methods loadPreviousResult and calculateResult remain unchanged. Please note how the shutdown hook uses synchronized and closes the resource after copying the intermediate result to be written to another variable. Copying is not strictly necessary if you want to keep the synchronised on Result open until after writing it to the file. In that case however you would need to be sure that the internal result state cannot be changed in any way by another thread, i.e. resource encapsulation is important.

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

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

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

  public void close() {
    closed = true;
  }

  @Override
  public String toString() {
    return &quot;Result{value=&quot; + value + &#39;}&#39;;
  }
}
import java.io.*;

public class ResultShutdownHookDemo {
  private static final File resultFile = new File(&quot;result.txt&quot;);
  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(&quot;Starting with intermediate result &quot; + result);
    }
    catch (IOException e) {
      System.err.println(&quot;Cannot read result, starting from scratch&quot;);
    }
  }

  private static void saveResultOnExit() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -&gt; {
      long resultToBeSaved;
      synchronized (result) {
        System.out.println(&quot;Shutting down after progress from &quot; + oldResult + &quot; to &quot; + 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(&quot;Cannot write result&quot;);
      }
    }));
  }

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

}

Now you see exceptions from the main thread on the console because it tries to continue working after the shutdown hook has closed the resource already. But this does not matter during shutdown and ensures that we know exactly what is written to the output file during shutdown and no other thread modifies the object to be written in the meantime.

my-path&gt; del result.txt
my-path&gt; 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 &quot;main&quot; 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&gt; 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 &quot;main&quot; 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&gt; 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 &quot;main&quot; java.lang.RuntimeException: resource closed
at Result.getValue(Result.java:7)
at ResultShutdownHookDemo.calculateResult(ResultShutdownHookDemo.java:51)
at ResultShutdownHookDemo.main(ResultShutdownHookDemo.java:11)

huangapple
  • 本文由 发表于 2020年10月26日 05:34:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/64529050.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定