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

发布于:2022-10-30 ⋅ 阅读:(386) ⋅ 点赞:(0)

一. 进程如何才会退出

  进程退出的原因有很多种,也可以分为很多类别。如下一些常见的操作都会导致进入退出阶段,右侧的正常退出和异常退出会在虚拟机关闭进程的执行一些程序当中注册的一些操作,而强制退出则直接关掉进程,进程的代码运行片段直接停留在接受到强制退出信号的那一刻了。所以强制退出并没有什么好讨论的,接下来主要讨论的是如何正常(优雅)退出进程。
进程退出常见原因
  当进程接收到正常退出和异常退出的信号时(强制退出时不会执行),会执行当前进程已经注册的关闭钩子(可以在程序中任何地方进行使用Runtime.getRuntime().addShutdownHook 注册),而注册钩子函数也是我们在程序当中释放关闭一些资源的常见做法。如下是 jdk 对于钩子函数的一些注释,我挑出几点需要关注的列出来:

1. 程序正常退出的场景:所有非守护线程执行完毕;程序中调用 System.exit();控制台中使用 ctrl+c 停止程序;用户登出操作系统
2. 钩子函数是一个已经初始化但是没有 start 的线程,会在进程关闭的时候调用所有的钩子函数
3. 钩子函数应该较快的完成操作,不应该做一些耗时的操作
  程序常见的退出场景中,所有非守护线程全部执行完毕才是我们日常开发中最容易遇到的场景。当然这里的执行完毕包括任务本身是常驻任务,但是设置一个中断标记让常驻任务在看到中断标记时自己退出。如一个线程while(isStop) 中,我们使用钩子函数标记 isStop为 true,当前线程退出循环不再存活。注意 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

参考文档:

  1. 如何优雅地停止Java进程
  2. 主线程异常会导致 JVM 退出?
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到