Java Shutdown Hook scenario usage and source code analysis

Posted by dizel247 on Mon, 31 Jan 2022 05:50:12 +0100

I'm Chen PI. I'm an ITer of Internet Coding. I search "Chen Pi's JavaLib" on wechat. I can read the latest article at the first time and reply to [data], so I can get my carefully sorted technical data, e-books, interview data of front-line large factories and excellent resume template.

background

If you want to do some extra processing work when the Java process exits, including normal and abnormal exits, such as resource cleaning, object destruction, memory data persistence to disk, waiting for the thread pool to handle all tasks, and so on. Especially when the process hangs abnormally, if some important states are not retained in time, or the tasks of the thread pool are not processed, it may cause serious problems. What should I do?

The Shutdown Hook in Java provides a better scheme. We can use Java Runtime. The addshutdown hook (thread hook) method registers the close hook with the JVM. Before the JVM exits, it will automatically call the execution hook method and do some ending operations, so as to make the process exit smoothly and gracefully and ensure the integrity of the business.


Introduction to Shutdown Hook

In fact, shutdown hook is a simple thread that has been initialized but not started. When the virtual machine starts to shut down, it will call all registered hooks. The execution of these hooks is concurrent and the execution order is uncertain.

In the process of virtual machine shutdown, you can continue to register new hooks or cancel the registered hooks. However, it is possible to throw IllegalStateException. The methods of registering and unregistering hooks are defined as follows:

public void addShutdownHook(Thread hook) {
	// ellipsis
}

public void removeShutdownHook(Thread hook) {
	// ellipsis
}

Close the scene where the hook is called

The close hook can be called in the following scenarios:

  1. The program exits normally
  2. The program calls system Exit() exit
  3. The terminal uses Ctrl+C to interrupt the program
  4. The program throws exceptions that cause the program to exit, such as OOM, array out of bounds and other exceptions
  5. System events, such as user logoff or system shutdown
  6. Use the Kill pid command to kill the process. Note that using kill -9 pid to forcibly kill the process will not trigger the execution hook

Normal exit of verification program

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method...")));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
The program is about to exit...
Execute hook method...

Process finished with exit code 0

The verifier calls system Exit() exit

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method...")));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.exit(-1);
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
Execute hook method...

Process finished with exit code -1

Verify that the terminal uses Ctrl+C to interrupt the program, run the program in the command line window, and then use Ctrl+C to interrupt

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method...")));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.out.println("The program is about to exit...");
    }
}

Operation results

D:\IdeaProjects\java-demo\java ShutdownHookDemo
 The program starts...
Execute hook method...

Demonstrate that throwing an exception causes the program to exit abnormally

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method...")));
    }

    public static void main(String[] args) {
        System.out.println("The program starts...");
        int a = 0;
        System.out.println(10 / a);
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
Execute hook method...
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.chenpi.ShutdownHookDemo.main(ShutdownHookDemo.java:12)

Process finished with exit code 1

If the system is shut down or the Kill pid command is used to kill the process, it will not be demonstrated. Those interested can verify by themselves.


matters needing attention

You can register multiple close hooks with the virtual machine, but note that the execution of these hooks is concurrent and the execution order is uncertain.

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method A...")));
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method B...")));
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method C...")));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
The program is about to exit...
Execute hook method B...
Execute hook method C...
Execute hook method A...

The hook method registered with the virtual machine needs to be completed as soon as possible. Try not to perform long-term operations, such as I/O and other operations that may be blocked, deadlock, etc., which will lead to the program not being closed for a short time, or even not being closed all the time. We can also introduce a timeout mechanism to force the exit hook to let the program end normally.

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // Simulate long-time operation
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.out.println("The program is about to exit...");
    }
}

The execution time of the above hooks is relatively long, which will eventually lead to the program being closed after waiting for a long time.


If the JVM has called to execute the process of closing the hook, it is not allowed to register a new hook and unregister the already registered hook, otherwise an IllegalStateException will be reported. Through source code analysis, when the JVM calls the hook, i.e. calls the ApplicationShutdownHooks#runHooks() method, it will take all hooks out of the variable hooks, and then set this variable to null.

// Call execution hook
static void runHooks() {
    Collection<Thread> threads;
    synchronized(ApplicationShutdownHooks.class) {
        threads = hooks.keySet();
        hooks = null;
    }

    for (Thread hook : threads) {
        hook.start();
    }
    for (Thread hook : threads) {
        try {
            hook.join();
        } catch (InterruptedException x) { }
    }
}

In the methods of registering and unregistering hooks, we will first judge whether the hooks variable is null. If it is null, an exception will be thrown.

// Registration hook
static synchronized void add(Thread hook) {
    if(hooks == null)
        throw new IllegalStateException("Shutdown in progress");

    if (hook.isAlive())
        throw new IllegalArgumentException("Hook already running");

    if (hooks.containsKey(hook))
        throw new IllegalArgumentException("Hook previously registered");

    hooks.put(hook, hook);
}
// Logoff hook
static synchronized boolean remove(Thread hook) {
    if(hooks == null)
        throw new IllegalStateException("Shutdown in progress");

    if (hook == null)
        throw new NullPointerException();

    return hooks.remove(hook) != null;
}

Let's demonstrate this

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Execute hook method...");
            Runtime.getRuntime().addShutdownHook(new Thread(
                    () -> System.out.println("stay JVM If a hook is newly registered during the process of calling the hook, an error will be reported IllegalStateException")));
            // Unregister the hook when the JVM calls the hook, and an error IllegalStateException will be reported
            Runtime.getRuntime().removeShutdownHook(Thread.currentThread());
        }));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        Thread.sleep(2000);
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
The program is about to exit...
Execute hook method...
Exception in thread "Thread-0" java.lang.IllegalStateException: Shutdown in progress
	at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:66)
	at java.lang.Runtime.addShutdownHook(Runtime.java:211)
	at com.chenpi.ShutdownHookDemo.lambda$static$1(ShutdownHookDemo.java:8)
	at java.lang.Thread.run(Thread.java:748)

If you call runtime getRuntime(). If the halt () method stops the JVM, the virtual machine will not call the hook.

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Execute hook method...")));
    }

    public static void main(String[] args) {
        System.out.println("The program starts...");
        System.out.println("The program is about to exit...");
        Runtime.getRuntime().halt(0);
    }
}

Operation results

The program starts...
The program is about to exit...

Process finished with exit code 0

If you want to terminate the hook method in execution, you can only call runtime getRuntime(). The halt () method forces the program to exit. In Linux environment, the kill -9 pid command can also be used to forcibly terminate and exit.

package com.chenpi;

public class ShutdownHookDemo {

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Start executing hook method...");
            Runtime.getRuntime().halt(-1);
            System.out.println("End execution hook method...");
        }));
    }

    public static void main(String[] args) {
        System.out.println("The program starts...");
        System.out.println("The program is about to exit...");
    }
}

Operation results

The program starts...
The program is about to exit...
Start executing hook method...

Process finished with exit code -1

If the program uses Java Security Managers and uses shutdown hooks, the security permission RuntimePermission("shutdown hooks") is required. Otherwise, SecurityException will be caused.


practice

For example, our program has customized a thread pool to receive and process tasks. If the program suddenly crashes and exits abnormally, all tasks of the thread pool may not have been processed. If the program exits directly without processing, it may lead to important problems such as data loss and business exceptions. Then the hook comes in handy.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ShutdownHookDemo {
	// Thread pool
    private static ExecutorService executorService = Executors.newFixedThreadPool(3);

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Start executing hook method...");
            // Close thread pool
            executorService.shutdown();
            try {
            	// Wait 60 seconds
                System.out.println(executorService.awaitTermination(60, TimeUnit.SECONDS));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("End execution hook method...");
        }));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("The program starts...");
        // Add 10 tasks to the thread pool
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            final int finalI = i;
            executorService.execute(() -> {
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task " + finalI + " execute...");
            });
            System.out.println("Task " + finalI + " is in thread pool...");
        }
    }
}

In the command line window, run the program. After the 10 tasks are submitted to the thread pool, the Ctrl+C interrupt program is used before the task has been completed. Finally, after the virtual machine closes, the closing hook is called, the thread pool is closed, and the execution of all tasks is completed by waiting for 60 seconds.


Application of Shutdown Hook in Spring

How is Shutdown Hook used in Spring. Through source code analysis, when the Springboot project starts, it will judge whether the value of registerShutdownHook is true by default. If it is true, it will register the closing hook with the virtual machine.

private void refreshContext(ConfigurableApplicationContext context) {
	refresh(context);
	if (this.registerShutdownHook) {
		try {
			context.registerShutdownHook();
		}
		catch (AccessControlException ex) {
			// Not allowed in some environments.
		}
	}
}

@Override
public void registerShutdownHook() {
	if (this.shutdownHook == null) {
		// No shutdown hook registered yet.
		this.shutdownHook = new Thread() {
			@Override
			public void run() {
				synchronized (startupShutdownMonitor) {
				    // Hook Method 
					doClose();
				}
			}
		};
		// The bottom layer still uses this method to register hooks
		Runtime.getRuntime().addShutdownHook(this.shutdownHook);
	}
}

In the method doClose, which closes the hook, some pre-processing work will be done before closing the virtual machine, such as destroying all single instance beans in the container, closing BeanFactory, publishing closing events, and so on.

protected void doClose() {
	// Check whether an actual close attempt is necessary...
	if (this.active.get() && this.closed.compareAndSet(false, true)) {
		if (logger.isDebugEnabled()) {
			logger.debug("Closing " + this);
		}

		LiveBeansView.unregisterApplicationContext(this);

		try {
			// Publish the closing event of Spring application context, and let the listener respond and handle it before the application is closed
			publishEvent(new ContextClosedEvent(this));
		}
		catch (Throwable ex) {
			logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
		}

		// Stop all Lifecycle beans, to avoid delays during individual destruction.
		if (this.lifecycleProcessor != null) {
			try {
			    // Execute the closing method of lifecycle processor
				this.lifecycleProcessor.onClose();
			}
			catch (Throwable ex) {
				logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
			}
		}

		// Destroy all singleton beans in the container
		destroyBeans();

		// Close BeanFactory
		closeBeanFactory();

		// Let subclasses do some final clean-up if they wish...
		onClose();

		// Reset local application listeners to pre-refresh state.
		if (this.earlyApplicationListeners != null) {
			this.applicationListeners.clear();
			this.applicationListeners.addAll(this.earlyApplicationListeners);
		}

		// Switch to inactive.
		this.active.set(false);
	}
}

We know that we can define beans, implement the DisposableBean interface, and override the destroy object destruction method. The destroy method is called in the Spring registered close hook. For example, we use the ThreadPoolTaskExecutor thread pool class of the Spring framework, which implements the DisposableBean interface and rewrites the destroy method to destroy the thread pool before the program exits. The source code is as follows:

@Override
public void destroy() {
	shutdown();
}

/**
 * Perform a shutdown on the underlying ExecutorService.
 * @see java.util.concurrent.ExecutorService#shutdown()
 * @see java.util.concurrent.ExecutorService#shutdownNow()
 */
public void shutdown() {
	if (logger.isInfoEnabled()) {
		logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
	}
	if (this.executor != null) {
		if (this.waitForTasksToCompleteOnShutdown) {
			this.executor.shutdown();
		}
		else {
			for (Runnable remainingTask : this.executor.shutdownNow()) {
				cancelRemainingTask(remainingTask);
			}
		}
		awaitTerminationIfNecessary(this.executor);
	}
}

Topics: Java jvm Multithreading