关于java:优雅停机

81次阅读

共计 10065 个字符,预计需要花费 26 分钟才能阅读完成。

什么叫优雅停机

简略说就是、在对利用过程发送进行指令之后、能保障正在执行的业务操作不受影响。

利用接管到进行指令之后的步骤应该是、进行接管拜访申请、期待曾经接管的申请解决实现、并能胜利返回、这时才真正进行利用。

就 Java 语言生态来说、底层技术是反对的、所以咱们能力实现在 Java 语言上各个 Web 容器的优雅停机。

对于 kill 命令

在 Linux 中 kill 指令负责杀死过程、其后能够紧跟一个数字,代表信号编号 signal。

执行 kill -l 能够打印出所有的信号编号。

 kill -l
 HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2

咱们比拟相熟的就是 kill -9 pid、这个命令能够了解为操作系统从内核级别强行杀死某个过程。

kill -15 pid 则能够了解为发送一个告诉、告知利用被动敞开。而咱们有时候通过ctrl+c 来杀掉过程其实这就相当于kill -2 pid,用于告诉前台过程终止过程。

Demo

 @SpringBootApplication
 public class JunitSpringBootApplication {
 ​
  public static void main(String[] args) {
  SpringApplication.run(JunitSpringBootApplication.class, args);
  Runtime.getRuntime().addShutdownHook(new Thread(){
  @Override
  public void run() {
  System.out.println(“ 执行 shutdown hook”); }
  });
  }
 }

 @RestController
 public class HiController implements DisposableBean {
  @Override
  public void destroy() throws Exception {
  System.out.println(“destroy bean…..”);
  }
 }

以 Jar 包的模式将利用运行起来。

而后别离应用 kill -15 pidctrl+c

 执行 shutdown hook
 destroy bean…..

kill -9 pid 命令则什么都没有输入。

源码

 Runtime.getRuntime().addShutdownHook(new Thread(){
  @Override
  public void run() {
  System.out.println(“ 执行 shutdown hook”);         }
  });

Runtime.class

 public void addShutdownHook(Thread hook) {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
  sm.checkPermission(new RuntimePermission(“shutdownHooks”));
  }
  ApplicationShutdownHooks.add(hook);
 }

ApplicationShutdownHooks.class

 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() {
  // 执行注册的 hooks
  runHooks();
  }
  }
  );
  hooks = new IdentityHashMap<>();
  } catch (IllegalStateException e) {
  // application shutdown hooks cannot be added if
  // shutdown is in progress.
  hooks = null;
  }
 }
 // ================================
 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);
 }
 // 从 Map 中获取对应的线程、启动执行、并期待其返回
 static void runHooks() {
  Collection<Thread> threads;
  synchronized(ApplicationShutdownHooks.class) {
  threads = hooks.keySet();
  hooks = null;
  }
 ​
  for (Thread hook : threads) {
  hook.start();
  }
  for (Thread hook : threads) {
  while (true) {
  try {
  hook.join();
  break;
  } catch (InterruptedException ignored) {
  }
  }
  }
 }

再进入 Shutdown.add

 private static final int MAX_SYSTEM_HOOKS = 10;
 private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
 static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
  synchronized (lock) {
  if (hooks[slot] != null)
  throw new InternalError(“Shutdown hook at slot ” + slot + ” already registered”);
 ​
  if (!registerShutdownInProgress) {
  if (state > RUNNING)
  throw new IllegalStateException(“Shutdown in progress”);
  } else {
  if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
  throw new IllegalStateException(“Shutdown in progress”);
  }
 ​
  hooks[slot] = hook;
  }
 }

能够看到最大的 Runnable 的个数是 10 个、然而咱们通过 ApplicationShutdownHooks 的 Map 寄存多个敞开前解决线程。

Shutdown.add 运行 Runnable

 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];
  }
  if (hook != null) hook.run();
  } catch(Throwable t) {
  if (t instanceof ThreadDeath) {
  ThreadDeath td = (ThreadDeath)t;
  throw td;
  }
  }
  }
 }

当咱们应用 kill -15 pid 或者 ctrl + c 的时候

 /* 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
  */
  // 这个办法会调起 runHooks 办法
  sequence();
  halt(status);
  }
 }

这个办法将会被执行。目前 Runnable 数组中指存在两个值、一个是 ApplicationShutdownHooks.class 搁置进去的、一个是 DeleteOnExitHook 搁置进去的(它的次要性能是删除某些文件)。

Spring Boot 的 hooks 注册

间接进入到 SpringApplication 中

进入到 refresh 办法中

org.springframework.context.support.AbstractApplicationContext#registerShutdownHook

 protected void doClose() {
  // Check whether an actual close attempt is necessary…
  if (this.active.get() && this.closed.compareAndSet(false, true)) {
  LiveBeansView.unregisterApplicationContext(this);
  publishEvent(new ContextClosedEvent(this));
 ​
  // Stop all Lifecycle beans, to avoid delays during individual destruction.
  if (this.lifecycleProcessor != null) {
  try {
  this.lifecycleProcessor.onClose();
  }
  catch (Throwable ex) {
 
  }
  }
  // 销毁单例 bean、destroy 办法就是这里被触发
  destroyBeans();
  // 敞开上下文以及 beanfactory
  closeBeanFactory();
  // 空实现、让子类去扩大
  onClose();
  // Switch to inactive.
  this.active.set(false);
  }
 }

所以咱们能够有以下几种形式在 JVM 敞开前被调用

  • 监听 ContextClosedEvent 事件
  • Bean 销毁的注解或者 Spring 的销毁的接口中
  • onClose 办法的重写

如何进行接管申请

只议论 Tomcat 作为 servlet 容器

实现以下该接口、获取 Tomcat 的 Connector

 @FunctionalInterface
 public interface TomcatConnectorCustomizer {
  /**
  * Customize the connector.
  * @param connector the connector to customize
  */
  void customize(Connector connector);
 }

而后监听 Spring 的敞开事件

 @Component
 public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
  private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
  private volatile Connector connector;
  private final int waitTime = 30;
  @Override
  public void customize(Connector connector) {
  this.connector = connector;
  }
  @Override
  public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
  this.connector.pause();
  Executor executor = this.connector.getProtocolHandler().getExecutor();
  if (executor instanceof ThreadPoolExecutor) {
  try {
  ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
  threadPoolExecutor.shutdown();
  if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
  log.warn(“Tomcat thread pool did not shut down gracefully within ” + waitTime + ” seconds. Proceeding with forceful shutdown”);
  }
  } catch (InterruptedException ex) {
  Thread.currentThread().interrupt();
  }
  }
  }
 }

对于 connector.pause() 执行之后、利用还是会承受新的申请,而后 hung 住,直到线程池被 shutdown、才会返回 connection peered。

其实比拟好的做法可能是滚动部署吧、在流量低的时间段、将流量导入到其中一部分实例中、残余局部不再有流量进入、而后敞开而后部署新的服务、确认没问题、将流量切换过去部署另一半。

如何敞开线程池

一个线程什么时候能够退出呢?当然只有线程本人能力晓得。

所以咱们这里要说的 Thread 的 interrrupt 办法,实质不是用来中断一个线程。是将线程设置一个中断状态。

1、如果此线程处于阻塞状态(比方调用了 wait 办法,io 期待),则会立马退出阻塞,并抛出 InterruptedException 异样,线程就能够通过捕捉 InterruptedException 来做肯定的解决,而后让线程退出。

2、如果此线程正处于运行之中,则线程不受任何影响,持续运行,仅仅是线程的中断标记被设置为 true。所以线程要在适当的地位通过调用 isInterrupted 办法来查看本人是否被中断,并做退出操作。

如果线程的 interrupt 办法先被调用,而后线程调用阻塞办法进入阻塞状态,InterruptedException 异样依旧会抛出。

如果线程捕捉 InterruptedException 异样后,持续调用阻塞办法,将不再触发 InterruptedException 异样。

线程池的敞开

线程池提供了两个敞开办法,shutdownNow 和 shuwdown 办法。

shutdownNow 办法的解释是:线程池拒接管新提交的工作,同时立马敞开线程池,线程池里的工作不再执行。

shutdown 办法的解释是:线程池拒接管新提交的工作,同时期待线程池里的工作执行结束后敞开线程池。

 public List<Runnable> shutdownNow() {
  List<Runnable> tasks;
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
  checkShutdownAccess();
  advanceRunState(STOP);
  interruptWorkers();
  tasks = drainQueue();
  } finally {
  mainLock.unlock();
  }
  tryTerminate();
  return tasks;
 }

advanceRunState(STOP); 将线程池的状态设置为 STOP

interruptWorkers(); 遍历线程池里的所有工作线程,而后调用线程的 interrupt 办法

 private void interruptWorkers() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
  for (Worker w : workers)
  w.interruptIfStarted();
  } finally {
  mainLock.unlock();
  }
 }

tasks = drainQueue(); 将还未执行的工作从队列中移除、返回给调用方

 private List<Runnable> drainQueue() {
  BlockingQueue<Runnable> q = workQueue;
  ArrayList<Runnable> taskList = new ArrayList<Runnable>();
  q.drainTo(taskList);
  if (!q.isEmpty()) {
  for (Runnable r : q.toArray(new Runnable[0])) {
  if (q.remove(r))
  taskList.add(r);
  }
  }
  return taskList;
 }

shutdownNow 之后线程池的反馈如何?

线程池的代码逻辑

 try {
  while (task != null || (task = getTask()) != null) {
  w.lock();
  // If pool is stopping, ensure thread is interrupted;
  // if not, ensure thread is not interrupted. This
  // requires a recheck in second case to deal with
  // shutdownNow race while clearing interrupt
  if ((runStateAtLeast(ctl.get(), STOP) ||
  (Thread.interrupted() &&
  runStateAtLeast(ctl.get(), STOP))) &&
  !wt.isInterrupted())
  wt.interrupt();
  try {
  beforeExecute(wt, task);
  Throwable thrown = null;
  try {
  task.run();
  } catch (RuntimeException x) {
  thrown = x; throw x;
  } catch (Error x) {
  thrown = x; throw x;
  } catch (Throwable x) {
  thrown = x; throw new Error(x);
  } finally {
  afterExecute(task, thrown);
  }
  } finally {
  task = null;
  w.completedTasks++;
  w.unlock();
  }
  }
  completedAbruptly = false;
 } finally {
  processWorkerExit(w, completedAbruptly);
 }

失常线程池就是在这个 for 循环中执行、如果工作正处于运行状态、即 task.run() 处于运行状态、即便线程被标识为 interrupt、然而不受影响继续执行。然而如果刚刚好处于阻塞状态、则会抛出 InterruptedException。抛出异样则会导致这个循环完结。

还有就是当 getTask 办法返回为 null 的时候也会完结循环

因为 showdownNow 的时候咱们将所有的工作线程都进行了 interrupt、所以当它处于在工作队列中阻塞获取工作的时候、其会被打断。

STOP = 536870912、SHUTDOWN=0。因为 shutdownNow 的时候将线程池的状态设置为 STOP、所以必定会进入第一个红框的逻辑中返回 null。

shutdown

 public void shutdown() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
  checkShutdownAccess();
  advanceRunState(SHUTDOWN);
  interruptIdleWorkers();
  onShutdown(); // hook for ScheduledThreadPoolExecutor
  } finally {
  mainLock.unlock();
  }
  tryTerminate();
 }

将线程池的状态设置为 SHUTDOWN

将闲暇的工作线程的标记位设置为 interrupt。如何判断其是否闲暇、通过 Lock、因为

不论是被调用了 interrupt 的线程还是没被调用的线程,什么时候退出呢?,这就要看 getTask 办法的返回是否为 null 了。

在 getTask 里的 if 判断 (上文中 getTask 代码截图中上边红色方框的代码) 中,因为线程池被 shutdown 办法批改为 SHUTDOWN 状态,SHUTDOWN 大于等于 SHUTDOWN 成立没问题,然而 SHUTDOWN 不在大于等于 STOP 状态,所以只有队列为空,getTask 办法才会返回 null,导致线程退出。

总结

  1. 当咱们调用线程池的 shutdownNow 时,

如果线程正在 getTask 办法中执行,则会通过 for 循环进入到 if 语句,于是 getTask 返回 null, 从而线程退出。不论线程池里是否有未实现的工作。

如果线程因为执行提交到线程池里的工作而处于阻塞状态,则会导致报错(如果工作里没有捕捉 InterruptedException 异样),否则线程会执行完当前任务,而后通过 getTask 办法返回为 null 来退出

  1. 当咱们调用线程池的 shuwdown 办法时,

如果线程正在执行线程池里的工作,即使工作处于阻塞状态,线程也不会被中断,而是继续执行。

如果线程池阻塞期待从队列里读取工作,则会被唤醒,然而会持续判断队列是否为空,如果不为空会持续从队列里读取工作,为空则线程退出。

最初还有一个要记得、shutdownNow 和 shutdown 调用完、线程池并不是立马敞开的想要期待线程池敞开、还须要调用 awaitTermination 办法来阻塞期待。

this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));

https://www.jianshu.com/p/0c49eb23c627

https://www.cnkirito.moe/gracefully-shutdown/

https://w.cnblogs.com/qingquanzi/p/9018627.html

正文完
 0