Search code examples
javaintellij-ideawhile-loopruntimeterminate

Java ShutdownHook not working as I expect it to


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.


Solution

  • 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 "Exit" button looking like an arrow from left to right pointing into an empty square - not the "Stop" button looking like a solid square just like on typical audio/video player. See also here and here for more information.


    Please look at the Javadoc for Runtime.addShutdownHook(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 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:

    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);
        }
      }
    
    }
    

    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> 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}
    

    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("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);
        }
      }
    
    }
    

    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> 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)