当前位置: 首页 > 科技观察

说说ShutdownHook的原理

时间:2023-03-18 22:30:37 科技观察

ShutdownHook介绍在java程序中,很容易在进程的最后添加一个钩子,即ShutdownHook。通常你可以在程序启动的时候加入如下代码和ShutdownHook我们可以在进程结束时做一些善后工作,比如释放占用的资源,保存程序状态等,为优雅(平滑)释放提供手段,在程序关闭前去除流量。许多java中间件或框架使用ShutdownHook功能。比如dubbo、spring等。Spring会在应用上下文加载的时候注册一个ShutdownHook。此ShutdownHook将在进程退出之前执行销毁bean和发出ContextClosedEvent等操作。dubbo监听spring框架下的ContextClosedEvent,调用dubboBootstrap.stop()清理现场,优雅的释放dubbo。spring的事件机制默认是同步的,所以可以等待所有的监听器完成发布事件的执行。ShutdownHook原理ShutdownHook数据结构和执行顺序当我们添加一个ShutdownHook时,它会调用ApplicationShutdownHooks.add(hook),到ApplicationShutdownHooks类下的静态变量privatestaticIdentityHashMap。当ApplicationShutdownHooks类被初始化时,钩子将被添加到Shutdown钩子中。Shutdownhooks是系统级的ShutdownHooks,系统级的ShutdownHook是由一个数组组成的,只能添加10个系统级的ShutdownHooks。系统级ShutdownHook调用线程类的run方法,所以系统级ShutdownHook是同步有序执行的privatestaticvoidrunHooks(){for(inti=0;ithreads;synchronized(ApplicationShutdownHooks.class){threads=hooks.keySet();hooks=null;}for(Threadhook:threads){hook.start();}for(Threadhook:threads){while(true){try{hook.join();break;}catch(InterruptedExceptionignored){}}}}用一张图总结如下:ShutdownHook触发点顺着Shutdown的runHooks的线索,得到如下两条调用路径重点关注Shutdown.exit和Shutdown.shutdownShutdown.exit跟踪Shutdown.exit的调用者,发现有Runtime.exit和Terminator.setupRuntime.exit是代码中主动结束进程的接口。Terminator.setup由initializeSystemClass调用。当第一个线程被初始化时,它被触发。触发后,注册一个信号监听函数,捕获kill发送的信号,调用Shutdown.exit结束进程。这就涵盖了代码中主动结束进程,通过kill杀掉进程的场景。不需要主动引入结束进程。这里讲一下信号捕获。在java中,我们可以编写如下代码来捕获kill信号。我们只需要实现SignalHandler接口和handle方法,在程序入口注册需要监听的信号即可。当然,并不是每个信号都可以被捕获和处理。publicclassSignalHandlerTestimplementsSignalHandler{publicstaticvoidmain(String[]args){Runtime.getRuntime().addShutdownHook(newThread(){@Overridepublicvoidrun(){System.out.println("I'mshutdownhook");}});SignalHandlersh=newSignalHandlerTest();Signal.handle(newSignal("HUP"),sh);Signal.handle(newSignal("INT"),sh);//Signal.handle(newSignal("QUIT"),sh);//这个信号不能capturedSignal.handle(newSignal("ABRT"),sh);//Signal.handle(newSignal("KILL"),sh);//无法捕获信号Signal.handle(newSignal("ALRM"),sh);Signal.handle(newSignal("TERM"),sh);while(true){System.out.println("mainrunning");try{Thread.sleep(2000L);}catch(InterruptedExceptione){e.printStackTrace();}}}@Overridepublicvoidhandle(Signalsignal){System.out.println("receivesignal"+signal.getName()+"-"+signal.getNumber());System.exit(0);}}注意,一般来说,我们捕获信号,在做一些个性化处理后,我们需要主动调用System.exit,否则进程不会退出。这时候我们只能使用kill-9来强行杀掉进程。而且每次信号捕获都在不同的线程中,所以它们之间的执行是异步的。Shutdown.shutdown方法可以在注释/*InvokedbytheJNIDestroyJavaVMprocedurewhenthelastnon-daemon*threadhasfinished.Unliketheexit方法,thismethoddoesnot*actuallyhalttheVM.*/翻译过来就是当最后一个非守护线程(非-守护线程)结束。java中有两种线程,用户线程和守护线程。守护线程为用户线程服务,例如GC线程。JVM判断是否结束的标志是是否还有用户线程在工作。Shutdown.shutdown在最后一个用户线程结束时被调用。这是JVM等虚拟机语言独有的“权利”。如果编译成golang之类的可执行二进制文件,则ShutdownHook不会在所有用户线程结束时执行。比如java进程正常退出时,代码中并没有主动终止进程,也没有kill,像这样(){System.out.println("I'mshutdownhook");}});}当主线程运行完毕,也可以打印出I'mshutdownhook。另一方面,golang无法做到这一点,通过上面的两个调用分析,我们总结出以下结论:我们可以看到,java的ShutdownHook其实是非常全面的,只有一个是覆盖不了的,就是当我们在杀进程的时候使用kill-9,因为程序无法捕获和处理,进程被阻塞了。直接kill,所以ShutdownHook无法执行。总结综上所述,我们得出了一些结论。重写捕获信号需要注意主动退出进程,否则进程可能永远不会退出。捕获信号的执行是异步的。用户级ShutdownHook与系统级ShutdownHook绑定,并且用户级异步执行,系统级同步执行,用户级在系统级执行顺序中排在第二位。ShutdownHook涵盖的范围很广,无论是手动调用接口退出进程,还是捕获信号退出进程,或者用户线程执行退出后,都会执行ShutdownHook。唯一不会执行的是kill-9