zoukankan      html  css  js  c++  java
  • 哦,这就是java的优雅停机?(实现及原理)

       优雅停机? 这个名词我是服的,如果抛开专业不谈,多好的名词啊!

      其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程、释放连接资源等。 

      再比如,就是不会让调用方的请求处理了一增,一下就中断了。而处理完本次后,再停止服务。

      Java语言中,我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册钩子,以保证程序平滑退出。(其他语言也类似)

    来个栗子:

    public class ShutdownGracefulTest {
    
        /**
         * 使用线程池处理任务
         */
        public static ExecutorService executorService = Executors.newCachedThreadPool();
    
        public static void main(String[] args) {
    
            //假设有5个线程需要执行任务
            for(int i = 0; i < 5; i++){
                final int id = i;
                Thread taski = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(System.currentTimeMillis() + " : thread_" + id + " start...");
                        try {
                            TimeUnit.SECONDS.sleep(id);
                        }
                        catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(System.currentTimeMillis() + " : thread_" + id + " finish!");
                    }
                });
                taski.setDaemon(true);
                executorService.submit(taski);
            }
    
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown hooking...");
                    boolean shutdown = true;
                    try {
                        executorService.shutdown();
                        System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() +  " shutdown signal got, wait threadPool finish.");
                        executorService.awaitTermination(1500, TimeUnit.SECONDS);
                        boolean done = false;
                        System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() +  " all thread's done.");
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                        // 尝试再次关闭
                        if(!executorService.isTerminated()) {
                            executorService.shutdownNow();
                        }
                    }
                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown done...");
                }
            }));
    
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown hooking...");
                        Thread.sleep(1000);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown done...");
                }
            }));
    
            System.out.println("main method exit...");
            System.exit(0);
        }
    }

    运行结果如下:

      很明显,确实是优雅了,虽然最后收到了一关闭信号,但是仍然保证了任务的处理完成。很棒吧!

      那么,在实际应用中是如何体现优雅停机呢?

    kill -15 pid 


    通过该命令发送一个关闭信号给到jvm, 然后就开始执行 Shutdown Hook 了,你可以做很多:
      1. 关闭 socket 链接
      2. 清理临时文件
      3. 发送消息通知给订阅方,告知自己下线
      4. 将自己将要被销毁的消息通知给子进程
      5. 各种资源的释放
      ...

      而在平时工作中,我们不乏看到很多运维同学,是这么干的:

    kill -9 pid

      如果这么干的话,jvm也无法了,kill -9 相当于一次系统宕机,系统断电。这会给应用杀了个措手不及,没有留给应用任何反应的机会。
      所以,无论如何是优雅不起来了。

      要优雅,是代码和运维的结合!

    其中,线程池的关闭方式为:

    executorService.shutdown();
    executorService.awaitTermination(1500, TimeUnit.SECONDS);

      ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。
      ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。
        shutdown() :启动顺序关闭,其中执行先前提交的任务,但不接受新任务。如果已经关闭,则调用没有附加效果。此方法不等待先前提交的任务完成执行。
        shutdownNow():尝试停止所有正在执行的任务,停止等待任务的处理,并返回正在等待执行的任务的列表。当从此方法返回时,这些任务将从任务队列中耗尽(删除)。此方法不等待主动执行的任务终止。

        executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)。


    注意:
      虚拟机会对多个shutdownhook以未知的顺序调用,都执行完后再退出。
      如果接收到 kill -15 pid 命令时,执行阻塞操作,可以做到等待任务执行完成之后再关闭 JVM。同时,也解释了一些应用执行 kill -15 pid 无法退出的问题,如:中断被阻塞了,或者hook运行了死循环代码。

    实现原理:

    Runtime.getRuntime().addShutdownHook(hook);        // 添加钩子,开启优雅之路

    // 具体流程如下:

        /**
         * Registers a new virtual-machine shutdown hook.
         *
         * @param   hook
         *          An initialized but unstarted <tt>{@link Thread}</tt> object
         *
         * @throws  IllegalArgumentException
         *          If the specified hook has already been registered,
         *          or if it can be determined that the hook is already running or
         *          has already been run
         *
         * @throws  IllegalStateException
         *          If the virtual machine is already in the process
         *          of shutting down
         *
         * @throws  SecurityException
         *          If a security manager is present and it denies
         *          <tt>{@link RuntimePermission}("shutdownHooks")</tt>
         *
         * @see #removeShutdownHook
         * @see #halt(int)
         * @see #exit(int)
         * @since 1.3
         */
        public void addShutdownHook(Thread hook) {
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                sm.checkPermission(new RuntimePermission("shutdownHooks"));
            }
            // 添加到 application 中
            ApplicationShutdownHooks.add(hook);
        }
        
        // java.lang.ApplicationShutdownHooks.add(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 以map类型保存, k->k 形式存储,保证每一个钩子都是独立的
            hooks.put(hook, hook);
        }
        
        // java.lang.ApplicationShutdownHooks 会先注册一个静态块,添加一个任务到 Shutdown 中
        /* The set of registered hooks */
        private static IdentityHashMap<Thread, Thread> hooks;
        static {
            try {
                Shutdown.add(1 /* shutdown hook invocation order */,
                    false /* not registered if shutdown in progress */,
                    new Runnable() {
                        public void run() {
                            // 即当该任务被调用时,调用自身的运行方法,使所有注册的 hook 运行起来
                            runHooks();
                        }
                    }
                );
                hooks = new IdentityHashMap<>();
            } catch (IllegalStateException e) {
                // application shutdown hooks cannot be added if
                // shutdown is in progress.
                hooks = null;
            }
        }
        
        // runHooks 执行所有钩子线程,进行异步调用
        /* Iterates over all application hooks creating a new thread for each
         * to run in. Hooks are run concurrently and this method waits for
         * them to finish.
         */
        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) { }
            }
        }

    到现在为止,我们已经知道关闭钩子是如何执行的,但是,还不是知道,该钩子是何时触发?

        // java.lang.Shutdown.add() 该方法会jvm主动调用,从而触发 后续钩子执行
        /* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
         * thread has finished.  Unlike the exit method, this method does not
         * actually halt the VM.
         */
        static void shutdown() {
            synchronized (lock) {
                switch (state) {
                case RUNNING:       /* Initiate shutdown */
                    state = HOOKS;
                    break;
                case HOOKS:         /* Stall and then return */
                case FINALIZERS:
                    break;
                }
            }
            synchronized (Shutdown.class) {
                // 执行序列
                sequence();
            }
        }
        // 而 sequence() 则会调用 runHooks(), 调用自定义的钩子任务
        private static void sequence() {
            synchronized (lock) {
                /* Guard against the possibility of a daemon thread invoking exit
                 * after DestroyJavaVM initiates the shutdown sequence
                 */
                if (state != HOOKS) return;
            }
            runHooks();
            boolean rfoe;
            synchronized (lock) {
                state = FINALIZERS;
                rfoe = runFinalizersOnExit;
            }
            if (rfoe) runAllFinalizers();
        }
        
        // 执行钩子,此处最多允许注册 10 个钩子,且进行同步调用,当然这是最顶级的钩子,钩子下还可以添加钩子,可以任意添加n个
        private static void runHooks() {
            for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
                try {
                    Runnable hook;
                    synchronized (lock) {
                        // acquire the lock to make sure the hook registered during
                        // shutdown is visible here.
                        currentRunningHook = i;
                        hook = hooks[i];
                    }
                    // 同步调用注册的hook, 即 前面看到 ApplicationShutdownHooks.runHooks()
                    if (hook != null) hook.run();
                } catch(Throwable t) {
                    if (t instanceof ThreadDeath) {
                        ThreadDeath td = (ThreadDeath)t;
                        throw td;
                    }
                }
            }
        }
        

    如此,整个关闭流程完美了。

    简化为: 

      1. 注册流程(应用主动调用)
        Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
      2. 执行流程(jvm自动调用)
        java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最终

  • 相关阅读:
    移动方法
    linux主编号的动态分配
    linux 分配和释放设备编号
    linux设备编号的内部表示
    linux主次编号
    linux模块参数
    linux scull 的设计
    linux模块加载竞争
    linux清理函数
    linux初始化中的错误处理
  • 原文地址:https://www.cnblogs.com/yougewe/p/9881874.html
Copyright © 2011-2022 走看看