Java 中如何优雅的关闭进程&线程

一. 进程如何才会退出

  进程退出的原因有很多种,也可以分为很多类别。如下一些常见的操作都会导致进入退出阶段,右侧的正常退出和异常退出会在虚拟机关闭进程的执行一些程序当中注册的一些操作,而强制退出则直接关掉进程,进程的代码运行片段直接停留在接受到强制退出信号的那一刻了。所以强制退出并没有什么好讨论的,接下来主要讨论的是如何正常(优雅)退出进程。主动退出和被动退出是我自己想到的一个分类,主动退出常用于一些定时任务,简单任务,内部达到某种条件的时候就会自动停止进程。被动退出则是一些常驻任务,如微服务接口,web 服务等等,任务正常状态是不会主动停止的,只有外部手动关闭他们,一般是使用 kill -15 来关闭,可以保证服务关闭的时候能够释放关闭一些资源
进程常见退出原因
  当进程接收到正常退出和异常退出的信号时(强制退出时不会执行),会执行当前进程已经注册的关闭钩子(可以在程序中任何地方进行使用Runtime.getRuntime().addShutdownHook 注册),而注册钩子函数也是我们在程序当中释放关闭一些资源的常见做法。注意异常退出时的 OOM killer 不同于 java 里面的 OOM Exception,文末放置了关于 OOM killer 的详细介绍。OOM Exception 虽然会导致许多线程不能正常工作,但是却并不会直接关闭进程,但是可能成为导致进程关闭的间接原因。如下是 jdk 对于钩子函数的一些注释,我挑出几点需要关注的列出来:

1. 程序正常退出的场景:所有非守护线程执行完毕;程序中调用 System.exit();控制台中使用 ctrl+c 停止程序;用户登出操作系统
2. 钩子函数是一个已经初始化但是没有 start 的线程,会在进程关闭的时候调用所有的钩子函数
3. 钩子函数应该较快的完成操作,不应该做一些耗时的操作
  程序常见的退出场景中,所有非守护线程全部执行完毕才是我们日常开发中最容易遇到的场景。当然这里的执行完毕包括任务本身是常驻任务,但是设置一个中断标记让常驻任务在看到中断标记时自己退出。如一个线程while(isRunning) 中,我们使用钩子函数标记 isRunning为 false,当前线程退出循环不再存活。注意 main 线程和默认创建的线程都是非守护线程,需要手动设置 setDaemon(true) 才是守护线程,如 java 进程中的垃圾回收线程就是守护线程。守护线程常做一些辅助性的任务,因为当一个进程中所有非守护线程结束后,进程会结束,守护线程会被强制结束

/**
* Registers a new virtual-machine shutdown hook.
*
 * <p> The Java virtual machine <i>shuts down</i> in response to two kinds
 * of events:
 *
 *   <ul>
 *
 *   <li> The program <i>exits</i> normally, when the last non-daemon
 *   thread exits or when the <tt>{@link #exit exit}</tt> (equivalently,
 *   {@link System#exit(int) System.exit}) method is invoked, or
 *
 *   <li> The virtual machine is <i>terminated</i> in response to a
 *   user interrupt, such as typing <tt>^C</tt>, or a system-wide event,
 *   such as user logoff or system shutdown.
 *
 *   </ul>
 *
 * <p> A <i>shutdown hook</i> is simply an initialized but unstarted
 * thread.  When the virtual machine begins its shutdown sequence it will
 * start all registered shutdown hooks in some unspecified order and let
 * them run concurrently.  When all the hooks have finished it will then
 * run all uninvoked finalizers if finalization-on-exit has been enabled.
 * Finally, the virtual machine will halt.  Note that daemon threads will
 * continue to run during the shutdown sequence, as will non-daemon threads
 * if shutdown was initiated by invoking the <tt>{@link #exit exit}</tt>
 * method.
 *
 * <p> Once the shutdown sequence has begun it can be stopped only by
 * invoking the <tt>{@link #halt halt}</tt> method, which forcibly
 * terminates the virtual machine.
 *
 * <p> Once the shutdown sequence has begun it is impossible to register a
 * new shutdown hook or de-register a previously-registered hook.
 * Attempting either of these operations will cause an
 * <tt>{@link IllegalStateException}</tt> to be thrown.
 *
 * <p> Shutdown hooks run at a delicate time in the life cycle of a virtual
 * machine and should therefore be coded defensively.  They should, in
 * particular, be written to be thread-safe and to avoid deadlocks insofar
 * as possible.  They should also not rely blindly upon services that may
 * have registered their own shutdown hooks and therefore may themselves in
 * the process of shutting down.  Attempts to use other thread-based
 * services such as the AWT event-dispatch thread, for example, may lead to
 * deadlocks.
 *
 * <p> Shutdown hooks should also finish their work quickly.  When a
 * program invokes <tt>{@link #exit exit}</tt> the expectation is
 * that the virtual machine will promptly shut down and exit.  When the
 * virtual machine is terminated due to user logoff or system shutdown the
 * underlying operating system may only allow a fixed amount of time in
 * which to shut down and exit.  It is therefore inadvisable to attempt any
 * user interaction or to perform a long-running computation in a shutdown
 * hook.
 *
 * <p> Uncaught exceptions are handled in shutdown hooks just as in any
 * other thread, by invoking the <tt>{@link ThreadGroup#uncaughtException
 * uncaughtException}</tt> method of the thread's <tt>{@link
 * ThreadGroup}</tt> object.  The default implementation of this method
 * prints the exception's stack trace to <tt>{@link System#err}</tt> and
 * terminates the thread; it does not cause the virtual machine to exit or
 * halt.
 *
 * <p> In rare circumstances the virtual machine may <i>abort</i>, that is,
 * stop running without shutting down cleanly.  This occurs when the
 * virtual machine is terminated externally, for example with the
 * <tt>SIGKILL</tt> signal on Unix or the <tt>TerminateProcess</tt> call on
 * Microsoft Windows.  The virtual machine may also abort if a native
 * method goes awry by, for example, corrupting internal data structures or
 * attempting to access nonexistent memory.  If the virtual machine aborts
 * then no guarantee can be made about whether or not any shutdown hooks
 * will be run. <p>
 *
 * @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"));
    }
    ApplicationShutdownHooks.add(hook);
}

二. 如何正确的使用钩子函数
  通过上文我们已经了解到,钩子函数是我们在正常退出程序时清理释放资源的常规操作。下面列举几种常见用法:
  1.使用 java 内置的线程中断标记 interrupt()
   interrupt() 用于在为某一个线程设置中断标记,假设在 A 线程里面为 B 线程设置 interrupt(),那么 B 线程可以处理这个中断也可以选择忽略这个中断标记,常见的 wait(),sleep(),join() 这些阻塞线程的方法都会在接受到中断标记之后马上抛出 InterruptedException(), 当前线程接收到中断异常之后可以选择进行处理或者调用 interrupted() 重置当前线程的中断标记(即忽略中断异常),使其可以继续被上述方法阻塞
   如下为进程接收到 kill -15 信号调用钩子函数标记 test 线程中断,而 test 线程处理中断标记释放资源并且退出循环结束的例子。此处如果没有 break 退出循环,test 线程照样会结束,上文提到过钩子函数执行时间不宜过长。因为结束进程是操作系统操作的,如果操作系统发现长时间无法关闭这个进程,那么就会强制关闭,继而导致其中的线程也会全部强制关闭。当然你也可以在钩子函数里面 sleep() 延迟一些关闭时间,好使其它线程能够完成中断之后的释放资源操作处理线程中断

public class CloseExampleTest {
  public static void main(String[] args) {
    Thread test = new Thread(() -> {
      System.out.println("open resource");
      while (true) {
        try {
          TimeUnit.SECONDS.sleep(5);
          System.out.println("process resource");
        } catch (InterruptedException e) {
          // 接受到进程关闭时的中断异常,释放一些资源
          System.out.println("close resource");
          // 退出循环,则执行完 run 方法的线程自动结束
          break;
        }
      }
    });
    test.start();
	// 在 main 线程里面注册钩子函数,当接受到正常退出进程的信号时执行钩子函数,注意 interrupt() 
	// 标记一般是在别的线程标记而不是在被阻塞的线程里面标记,此处是在钩子函数线程里面标记 test 线程的中断,通知 test 线程本身做出反应
    Runtime.getRuntime().addShutdownHook(new Thread(test::interrupt));
  }
}

   如果线程选择忽略中断标记那?等待一些资源释放时线程也会选择阻塞,而此时被中断异常换线的线程代表资源已经就绪,在被唤醒之后应该重置中断标记,不然这个线程处理完资源之后就不能再被上面的 sleep() 等方法阻塞了,调用之后立即就会抛出 InterruptedException,调用 Thread.interrupted(); 之后重置中断标记然后阻塞等待下次资源继续就绪

public class CloseExampleTest {
  public static void main(String[] args) throws InterruptedException {
    Thread test = new Thread(() -> {
      System.out.println("open resource");
      while (true) {
        try {
          // 这里睡眠多久已经不重要,如果没有中断标记,这个线程永远不会 process resource
          TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
          // 重置中断标记,使当前线程后续依然可以被 sleep 等方法阻塞
          Thread.interrupted();
          System.out.println("process resource");
        }
      }
    });
    test.start();
    // 产生三次中断标记,则 test 只会处理三次资源,三次之后 main 线程执行完毕死亡,但是 test 线程依然在阻塞,所以进程依然不会退出
    for (int i = 0; i < 3; i++) {
      test.interrupt();
      TimeUnit.SECONDS.sleep(10);
    }
  }
}
//open resource
//process resource
//process resource
//process resource

  2.自定义中断标记
   上面最后一个例子里面我们已经使用 interrupt 作为资源就绪的唤醒标记,那么在最后的三次执行完毕之后我们应该如何通知 test 结束线程那?此处我们就需要自定义一个中断标记 isRunning 来结束线程。除了此处 interrupt 被用作它处不再作为结束线程的标记之外,如果 test 线程里面没有能够抛出 InterruptedException 的地方,那么我们也只能使用自定义的中断标记。一般来说,除了上面提到的 sleep 等方法会抛出 InterruptedException 之外,InterruptedException 作为规范被很多库作为阻塞方法常见的抛出异常之一,如 KafkaConsumer.poll(),BlockingQueue.put(),BlockingQueue.take()… 所以日常开发中如果使用 interrupt 就足够的情况下,也就无需我们自己定义额外的中断标记了

public class CloseExampleTest {
  public static void main(String[] args) throws InterruptedException {
    final AtomicBoolean isRunning = new AtomicBoolean(true);

    Thread test = new Thread(() -> {
      System.out.println("open resource");
      while (isRunning.get()) {
        try {
          TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
          Thread.interrupted();
          System.out.println("process resource");
        }
      }
      System.out.println("close resource");
    });
    test.start();

    for (int i = 0; i < 3; i++) {
      test.interrupt();
      TimeUnit.SECONDS.sleep(10);
    }
    isRunning.set(false);
  }
}
//open resource
//process resource
//process resource
//process resource
//close resource

// Process finished with exit code 0 内部所有非守护线程执行完毕结束进程

  3.为钩子函数选择合适的时间
   当所有的钩子函数执行完毕,非守护线程也就会被强制关闭。如下所示,当忽略了中断标记之后,test 线程依然在循环中。此时显然 test 线程是不可能自动结束的,所以在钩子函数执行完毕之后,test 线程依然被强制关闭了。我们在实际开发过程中,如果预料到某一个线程的关闭需要多长时间,那么可以主动等待一些时间去让线程主动完成释放资源的操作,而不是被进程强制的关闭

public class CloseExampleTest {
  public static void main(String[] args) {

    Thread test = new Thread(() -> {
      System.out.println("open resource");
      while (true) {
        try {
          System.out.println("process resource");
          TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
          System.out.println("receive interrupt flag");
          // 重置中断标记,代表忽略了这个中断标记
          Thread.interrupted();
        }
      }
    });
    test.start();

    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      test.interrupt();
      try {
        // 等待 30s 后再结束钩子函数
        TimeUnit.SECONDS.sleep(30);
      } catch (InterruptedException ignore) {
      }
    }));
    // 内部调用主动正常退出进程
    System.exit(0);
  }
}
// 可以看到在接受到中断标记之后,test 线程依然在运行,在钩子函数完成等待 30s 之后关闭
//open resource
//process resource
//receive interrupt flag
//process resource
//process resource
//process resource
//process resource
//process resource
//process resource

//Process finished with exit code 0 idea 中程序退出显示

   最后,贴上 JDK 里面为什么在执行完所有钩子函数之后,依然存活的线程会直接结束的代码(java.lang.Shutdown),有兴趣可以看一下文末参考文档 4.JVM源码系列:java 中关于自定义信号在linux下的实现 里面的详细剖析

// java.lang.Shutdown#exit
/* Invoked by Runtime.exit, which does all the security checks.
 * Also invoked by handlers for system-provided termination events,
 * which should pass a nonzero status code.
 */
static void exit(int status) {
    boolean runMoreFinalizers = false;
    synchronized (lock) {
        if (status != 0) runFinalizersOnExit = false;
        switch (state) {
        case RUNNING:       /* Initiate shutdown */
            state = HOOKS;
            break;
        case HOOKS:         /* Stall and halt */
            break;
        case FINALIZERS:
            if (status != 0) {
                /* Halt immediately on nonzero status */
                halt(status);
            } else {
                /* Compatibility with old behavior:
                 * Run more finalizers and then halt
                 */
                runMoreFinalizers = runFinalizersOnExit;
            }
            break;
        }
    }
    if (runMoreFinalizers) {
        runAllFinalizers();
        halt(status);
    }
    synchronized (Shutdown.class) {
        /* Synchronize on the class object, causing any other thread
         * that attempts to initiate shutdown to stall indefinitely
         */
        sequence(); // 执行完所有注册的钩子函数
        halt(status); // 进程强制结束
    }
}

/* The actual shutdown sequence is defined here.
 *
 * If it weren't for runFinalizersOnExit, this would be simple -- we'd just
 * run the hooks and then halt.  Instead we need to keep track of whether
 * we're running hooks or finalizers.  In the latter case a finalizer could
 * invoke exit(1) to cause immediate termination, while in the former case
 * any further invocations of exit(n), for any n, simply stall.  Note that
 * if on-exit finalizers are enabled they're run iff the shutdown is
 * initiated by an exit(0); they're never run on exit(n) for n != 0 or in
 * response to SIGINT, SIGTERM, etc.
 */
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();
}

参考文档:

  1. 如何优雅地停止Java进程
  2. 主线程异常会导致 JVM 退出?
  3. Linux内核OOM killer机制
  4. JVM源码系列:java 中关于自定义信号在linux下的实现
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值