1.什么是ShutdownHook

在Java程序中可以通过添加关闭钩子,实现在程序退出时关闭资源、平滑退出的功能。 使用Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:

  • 程序正常退出

  • 使用System.exit()

  • 终端使用Ctrl+C触发的中断

  • 系统关闭

  • 使用Kill pid命令干掉进程

2.java进程平滑退出的意义

很多时候,我们会有这样的一些场景,比如说nginx反向代理若干个负载均衡的web容器,又或者微服务架构中存在的若干个服务节点,需要进行无间断的升级发布。 在重启服务的时候,除非我们去变更nginx的配置,否则重启很可能会导致正在执行的线程突然中断,本来应该要完成的事情只完成了一半,并且调用方出现错误警告。 如果能有一种简单的方式,能够让进程在退出时能执行完当前正在执行的任务,并且让服务的调用方将新的请求定向到其他负载节点,这将会很有意义。 自己注册ShutdownHook可以帮助我们实现java进程的平滑退出。

3.java进程平滑退出的思路

(1) 在服务启动时注册自己的ShutdownHook (2) ShutdownHook在被运行时,首先不接收新的请求,或者告诉调用方重定向到其他节点 (3) 等待当前的执行线程运行完毕,如果五秒后仍在运行,则强制退出

4.如何屏敝第三方组件的ShutdownHook

4.1问题提出

我们会发现,有一些第三方组件在代码中注册了关闭自身资源的ShutdownHook,这些ShutdownHook对于我们的平滑退出有时候起了反作用。 比如dubbo,在static方法块里面注册了自己的关闭钩子,完全不可控。在进程退出时直接就把长连接给断开了,导致当前的执行线程无法正常完成,源码如下: AbstractConfig.java

static {Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {public void run() {if (logger.isInfoEnabled()) {logger.info("Run shutdown hook now.");}ProtocolConfig.destroyAll();}}, "DubboShutdownHook"));
}

4.2分析源码

这里先给出两个基本的类的源码: Runtime.java中相关方法源码

public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}ApplicationShutdownHooks.add(hook);
}
public boolean removeShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}return ApplicationShutdownHooks.remove(hook);
}

ApplicationShutdownHooks.java

class ApplicationShutdownHooks {/* 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() {runHooks();}});hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {// application shutdown hooks cannot be added if// shutdown is in progress.hooks = null;}}private ApplicationShutdownHooks() {}/* Add a new shutdown hook. Checks the shutdown state and the hook itself,* but does not do any security checks.*/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);}/* Remove a previously-registered hook. Like the add method, this method* does not do any security checks.*/static synchronized boolean remove(Thread hook) {if(hooks == null)throw new IllegalStateException("Shutdown in progress");if (hook == null)throw new NullPointerException();return hooks.remove(hook) != null;}/* 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) { }}}
}

从Runtime.java和ApplicationShutdownHooks.java的源码中,我们看到并没有一个可以遍历操作shutdownHook的方法。 Runtime.java仅有的一个removeShutdownHook的方法,对于未写线程名的匿名类来说,无法获取对象的引用,也无法分辨出彼此。 ApplicationShutdownHooks.java不是public的,类中的hooks也是private的,只有通过反射的方式才能获取并控制它们。

4.3解决方式

通过反射的方式注入自己的ShutdownHook并清除其他Thread

String className = "java.lang.ApplicationShutdownHooks";
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField("hooks");
field.setAccessible(true);
Thread shutdownThread = new Thread(new Runnable() {@Overridepublic void run() {// TODO}
});
shutdownThread.setName("XXX-WebShutdownThread");
IdentityHashMap<Thread, Thread> excludeIdentityHashMap = new ExcludeIdentityHashMap<>();
excludeIdentityHashMap.put(shutdownThread, shutdownThread);
synchronized (clazz) {IdentityHashMap<Thread, Thread> map = (IdentityHashMap<Thread, Thread>) field.get(clazz);for (Thread thread : map.keySet()) {Log.info("found shutdownHook: " + thread.getName());excludeIdentityHashMap.put(thread, thread);}field.set(clazz, excludeIdentityHashMap);
}

其中ExcludeIdentityHashMap的相关实现如下: 定义ExcludeIdentityHashMap类来帮助我们阻止非自己的ShutdownHook注入,只针对服务我们命令规则的XXXHook进行控制 ExcludeIdentityHashMap.java

class ExcludeIdentityHashMap<K,V> extends IdentityHashMap<K,V> {public V put(K key, V value) {if (key instanceof Thread) {Thread thread = (Thread) key;if (!thread.getName().startsWith("XXX")) {return value;}}return super.put(key, value);}
}

5.实现服务的平滑退出

对于生产业务的服务来说,目前只有这几种任务的入口:Http请求、dubbo请求、RabbitMQ消费、Quartz任务。

5.1 Http请求

测试发现Jetty容器在stop的时候不能实现平滑退出,springboot默认使用的tomcat容器可以,以下是部分代码示例: TomcatShutdownHook.java

EmbeddedWebApplicationContext embeddedWebApplicationContext = (EmbeddedWebApplicationContext) applicationContext;
EmbeddedServletContainer embeddedServletContainer = embeddedWebApplicationContext.getEmbeddedServletContainer();
if (embeddedServletContainer instanceof TomcatEmbeddedServletContainer) {Connector[] connectors = tomcatEmbeddedServletContainer.getTomcat().getService().findConnectors();for (Connector connector : connectors) {connector.pause();}for (Connector connector : connectors) {Executor executor = connector.getProtocolHandler().getExecutor();if (executor instanceof ThreadPoolExecutor) {try {ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;threadPoolExecutor.shutdown();if (!threadPoolExecutor.awaitTermination(5, TimeUnit.SECONDS)) {log.warn("Tomcat thread pool did not shutdown gracefully within 5 seconds. Proceeding with forceful shutdown");}} catch (InterruptedException e) {log.warn("TomcatShutdownHook interrupted", e);}}}
}

Connector首先parse(),所有新的请求都会收到connection reset的SocketException,nginx和ribbon默认情况下都会重试下一个服务; threadPoolExecutor等待线程池中的线程5秒,结束后退出。 源码地址:https://github.com/shunyang/springcloud-starter/tree/master/tomcat-shutdown-spring-boot-starter

5.2 dubbo请求

尝试了许多次,看了相关的源码,dubbo不支持平滑退出;解决方法只有一个,那就是修改dubbo的源码,以下两个地址有详细介绍: http://frankfan915.iteye.com/blog/2254097 https://my.oschina.net/u/1398931/blog/790709

5.3 RabbitMQ消费

以下是SpringBoot的示例,不使用Spring原理也是一样的 RabbitShutdownHook.java

RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry = applicationContext.getBean(RabbitListenerConfigUtils.RABBIT_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,RabbitListenerEndpointRegistry.class);
Collection<MessageListenerContainer> containers = rabbitListenerEndpointRegistry.getListenerContainers();
for (MessageListenerContainer messageListenerContainer : containers) {messageListenerContainer.stop();
}

5.4 Quartz任务

QuartzShutdownHook.java

Scheduler scheduler = applicationContext.getBean(Scheduler.class);
scheduler.shutdown(true);

6.为何重启时有时会有ClassNotFoundException

springboot通过java -jar example.jar的方式启动项目,在使用脚本restart的时候,首先覆盖旧的jar包,然后stop旧线程,启动新线程,这样就可能会出现此问题。 因为在stop的时候,ShutdownHook线程被唤醒,在其执行过程中,某些类(尤其是匿名类)还未加载,这时候就会通知ClassLoader去加载; ClassLoader持有的是旧jar包的文件句柄,虽然新旧jar包的名字路径完全一样,但是ClassLoader仍然是使用open着的旧jar包文件,文件已经找不到了,所以类加载不了就ClassNotFound了。 如何解决呢?也许有更优雅的方式,但是我没有找到;但是我们可以简单地把顺序调整一下,先stop、再copy覆盖、最后start,这样就OK了。

原作者的公众号:程序员小阳的程序人生

ShutdownHook - java中优雅地停止服务相关推荐

  1. Vue项目开发中优雅的切换服务端ip

    Vue项目开发中优雅的切换服务端ip 在进行Vue开发的时候,需要配置项目对应服务端的ip地址,但如果需要在多个服务端间进行切换,通常的做法是:手动修改vue.config.js配置文件中的服务端ip ...

  2. java中阿里短信服务(附带随机短信验证码生成类) --菜鸟小回

    java中阿里短信服务(短信验证码) 文章目录 java中阿里短信服务(短信验证码) 1. 创建签名 2. 创建模板 3. 测试验证码功能 4. 查看Api Demo 5. 获取AK信息 6. 复制A ...

  3. JDK ShutdownHook - 优雅地停止服务

    一.什么是ShutdownHook? 在Java程序中可以通过添加关闭钩子,实现在程序退出时关闭资源.平滑退出的功能.  使用Runtime.addShutdownHook(Thread hook)方 ...

  4. springboot项目优雅的停止服务

    springboot项目在启动的时候,使用java -jar  styy_auth_server.jar 比如我自己在公司测试服务器上写的启动文件start.sh #!/bin/sh nohup ja ...

  5. 在Java中构建响应式微服务系统——第三章 构建响应式微服务

    第三章 构建响应式微服务 在本章中,我们将使用Vert.x构建我们的第一个微服务.由于大多数微服务系统使用HTTP进行交互,因此我们将以HTTP微服务作为开始.但是由于系统包含多个相互通讯的微服务,因 ...

  6. java想要生成 字符串,如何在Java中“优雅地”生成String?

    我想生成一个字符串,如sql命令: "INSERT INTO xxx VALUES(XXX, XXX, XXX)" 目前我使用StringBuilder和一些String常量,如& ...

  7. ruby和python_Ruby,Python和Java中的Web服务

    ruby和python 今天,我不得不准备一些示例来说明Web服务是可互操作的. 因此,我已经使用Metro使用Java创建了一个简单的Web服务,并在Tomcat上启动了它. 然后尝试使用Pytho ...

  8. Ruby,Python和Java中的Web服务

    今天,我不得不准备一些示例来说明Web服务是可互操作的. 因此,我已经使用Metro使用Java创建了一个简单的Web服务,并在Tomcat上启动了它. 然后尝试使用Python和Ruby消耗它们. ...

  9. java web 教程_Java Web服务教程

    java web 教程 Welcome to the Java Web Services Tutorial. Here we will learn about web services, useful ...

最新文章

  1. 分享:Hadoop的Python框架指南
  2. 全球及中国苯基异丙基聚二甲基硅氧烷市场需求调查与竞争格局策略报告2022版
  3. Android 高级编程【6个实战案例(附源码):刮刮卡、补间动画、逐帧动画、Fragment、RecyclerView、下拉刷新】
  4. 学以致用二十二-----写一个基本环境设置的脚本
  5. 【 全干货 】5 分钟带你看懂 Docker ! 1
  6. c 程序设计语言第1 3部分,《C程序设计语言(第2版新版)典藏版》 —1.3 for语句...
  7. 趋势网盘点:语音识别技术创新
  8. win10计算器计算反三角函数
  9. java keytool 工具
  10. 非平稳序列的随机分析
  11. 免费的中文语音数据集汇总列表
  12. pyplot 使用 latex 报错:xelatex not found……
  13. C#中属性PropertyInfo的setvalue方法
  14. Nature子刊:高通量蛋白质组学方法学综述
  15. 库存明细帐处理示例(包含结存数).sql
  16. Google SketchUp Cookbook: (Chapter 3) Intersection Edges: Cutting and Trimming
  17. 看到强烈的太阳光你会不由自主的打喷嚏吗?
  18. 免费领百度网盘会员!抓紧!
  19. 20175212童皓桢 实验三敏捷开发与XP实践实验报告
  20. 墨者学院Tomcat 远程代码执行漏洞利用

热门文章

  1. [bzoj3673/3674可持久化并查集加强版]
  2. c++运算符重载总结
  3. linu逻辑分区动态调整大小
  4. Cypress USB开发文档列表(积累中)
  5. JavaMail学习笔记(一)、理解邮件传输协议(SMTP、POP3、IMAP、MIME)
  6. Linux的文件系统
  7. 数据结构与算法:13 字符串与整数集合
  8. 【怎样写代码】参数化类型 -- 泛型(七):泛型方法
  9. 如何利用C#开发“扫雷”小游戏
  10. 训练 GPT-3,为什么原有的深度学习框架吃不消?