作者:张乎兴
来源:Dubbo官方博客

背景介绍

Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在Spring Boot应用场景的开发。同时也整合了 Spring Boot 特性:

  • 自动装配 (比如: 注解驱动, 自动装配等).

  • Production-Ready (比如: 安全, 健康检查, 外部化配置等).

DubboConsumer启动分析

你有没有想过一个问题? incubator-dubbo-spring-boot-project中的 DubboConsumerDemo应用就一行代码, main方法执行完之后,为什么不会直接退出呢?


  1. @SpringBootApplication(scanBasePackages = "com.alibaba.boot.dubbo.demo.consumer.controller")

  2. public class DubboConsumerDemo {

  3.    public static void main(String[] args) {

  4.        SpringApplication.run(DubboConsumerDemo.class,args);

  5.    }

  6. }

其实要回答这样一个问题,我们首先需要把这个问题进行一个抽象,即一个JVM进程,在什么情况下会退出?

以Java 8为例,通过查阅JVM语言规范[1],在12.8章节中有清晰的描述:

A program terminates all its activity and exits when one of two things happens:

  • All the threads that are not daemon threads terminate.

  • Some thread invokes the exit method of class Runtime or class System, and the exitoperation is not forbidden by the security manager.

也就是说,导致JVM的退出只有2种情况:

  1. 所有的非daemon进程完全终止

  2. 某个线程调用了 System.exit()或 Runtime.exit()

因此针对上面的情况,我们判断,一定是有某个非daemon线程没有退出导致。我们知道,通过jstack可以看到所有的线程信息,包括他们是否是daemon线程,可以通过jstack找出那些是非deamon的线程。


  1. ➜  jstack 57785 | grep tid | grep -v "daemon"

  2. "container-0" #37 prio=5 os_prio=31 tid=0x00007fbe312f5800 nid=0x7103 waiting on condition  [0x0000700010144000]

  3. "container-1" #49 prio=5 os_prio=31 tid=0x00007fbe3117f800 nid=0x7b03 waiting on condition  [0x0000700010859000]

  4. "DestroyJavaVM" #83 prio=5 os_prio=31 tid=0x00007fbe30011000 nid=0x2703 waiting on condition  [0x0000000000000000]

  5. "VM Thread" os_prio=31 tid=0x00007fbe3005e800 nid=0x3703 runnable

  6. "GC Thread#0" os_prio=31 tid=0x00007fbe30013800 nid=0x5403 runnable

  7. "GC Thread#1" os_prio=31 tid=0x00007fbe30021000 nid=0x5303 runnable

  8. "GC Thread#2" os_prio=31 tid=0x00007fbe30021800 nid=0x2d03 runnable

  9. "GC Thread#3" os_prio=31 tid=0x00007fbe30022000 nid=0x2f03 runnable

  10. "G1 Main Marker" os_prio=31 tid=0x00007fbe30040800 nid=0x5203 runnable

  11. "G1 Conc#0" os_prio=31 tid=0x00007fbe30041000 nid=0x4f03 runnable

  12. "G1 Refine#0" os_prio=31 tid=0x00007fbe31044800 nid=0x4e03 runnable

  13. "G1 Refine#1" os_prio=31 tid=0x00007fbe31045800 nid=0x4d03 runnable

  14. "G1 Refine#2" os_prio=31 tid=0x00007fbe31046000 nid=0x4c03 runnable

  15. "G1 Refine#3" os_prio=31 tid=0x00007fbe31047000 nid=0x4b03 runnable

  16. "G1 Young RemSet Sampling" os_prio=31 tid=0x00007fbe31047800 nid=0x3603 runnable

  17. "VM Periodic Task Thread" os_prio=31 tid=0x00007fbe31129000 nid=0x6703 waiting on condition

此处通过grep tid 找出所有的线程摘要,通过grep -v找出不包含daemon关键字的行

通过上面的结果,我们发现了一些信息:

  • 有两个线程 container-0container-1非常可疑,他们是非daemon线程,处于wait状态

  • 有一些GC相关的线程,和VM打头的线程,也是非daemon线程,但他们很有可能是JVM自己的线程,在此暂时忽略。

综上,我们可以推断,很可能是因为 container-0container-1导致JVM没有退出。现在我们通过源码,搜索一下到底是谁创建的这两个线程。

通过对spring-boot的源码分析,我们在 org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerstartDaemonAwaitThread找到了如下代码


  1.    private void startDaemonAwaitThread() {

  2.        Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

  3.            @Override

  4.            public void run() {

  5.                TomcatEmbeddedServletContainer.this.tomcat.getServer().await();

  6.            }

  7.        };

  8.        awaitThread.setContextClassLoader(getClass().getClassLoader());

  9.        awaitThread.setDaemon(false);

  10.        awaitThread.start();

  11.    }

在这个方法加个断点,看下调用堆栈:


  1. initialize:115, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  2. <init>:84, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  3. getTomcatEmbeddedServletContainer:554, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)

  4. getEmbeddedServletContainer:179, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)

  5. createEmbeddedServletContainer:164, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  6. onRefresh:134, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  7. refresh:537, AbstractApplicationContext (org.springframework.context.support)

  8. refresh:122, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  9. refresh:693, SpringApplication (org.springframework.boot)

  10. refreshContext:360, SpringApplication (org.springframework.boot)

  11. run:303, SpringApplication (org.springframework.boot)

  12. run:1118, SpringApplication (org.springframework.boot)

  13. run:1107, SpringApplication (org.springframework.boot)

  14. main:35, DubboConsumerDemo (com.alibaba.boot.dubbo.demo.consumer.bootstrap)

可以看到,spring-boot应用在启动的过程中,由于默认启动了Tomcat暴露HTTP服务,所以执行到了上述方法,而Tomcat启动的所有的线程,默认都是daemon线程,例如监听请求的Acceptor,工作线程池等等,如果这里不加控制的话,启动完成之后JVM也会退出。因此需要显式地启动一个线程,在某个条件下进行持续等待,从而避免线程退出。Spring Boot 2.x 启动全过程源码分析(全),这篇文章推荐大家看下。

下面我们在深挖一下,在Tomcat的 this.tomcat.getServer().await()这个方法中,线程是如何实现不退出的。这里为了阅读方便,去掉了不相关的代码。


  1. public void await() {

  2.        // ...

  3.        if( port==-1 ) {

  4.            try {

  5.                awaitThread = Thread.currentThread();

  6.                while(!stopAwait) {

  7.                    try {

  8.                        Thread.sleep( 10000 );

  9.                    } catch( InterruptedException ex ) {

  10.                        // continue and check the flag

  11.                    }

  12.                }

  13.            } finally {

  14.                awaitThread = null;

  15.            }

  16.            return;

  17.        }

  18.        // ...

  19.    }

在await方法中,实际上当前线程在一个while循环中每10秒检查一次 stopAwait这个变量,它是一个 volatile 类型变量,用于确保被另一个线程修改后,当前线程能够立即看到这个变化。如果没有变化,就会一直处于while循环中。这就是该线程不退出的原因,也就是整个spring-boot应用不退出的原因。

因为Springboot应用同时启动了8080和8081(management port)两个端口,实际是启动了两个Tomcat,因此会有两个线程 container-0container-1

接下来,我们再看看,这个Spring-boot应用又是如何退出的呢?

DubboConsumer退出分析

在前面的描述中提到,有一个线程持续的在检查 stopAwait这个变量,那么我们自然想到,在Stop的时候,应该会有一个线程去修改 stopAwait,打破这个while循环,那又是谁在修改这个变量呢?

通过对源码分析,可以看到只有一个方法修改了 stopAwait,即 org.apache.catalina.core.StandardServer#stopAwait,我们在此处加个断点,看看是谁在调用。

注意,当我们在Intellij IDEA的Debug模式,加上一个断点后,需要在命令行下使用 kill-s INT $PID或者 kill-s TERM $PID才能触发断点,点击IDE上的Stop按钮,不会触发断点。这是IDEA的bug。在 IDEA 中调试 Bug,真是太厉害了!这个推荐大家看下。

可以看到有一个名为 Thread-3的线程调用了该方法:


  1. stopAwait:390, StandardServer (org.apache.catalina.core)

  2. stopInternal:819, StandardServer (org.apache.catalina.core)

  3. stop:226, LifecycleBase (org.apache.catalina.util)

  4. stop:377, Tomcat (org.apache.catalina.startup)

  5. stopTomcat:241, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  6. stop:295, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  7. stopAndReleaseEmbeddedServletContainer:306, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  8. onClose:155, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  9. doClose:1014, AbstractApplicationContext (org.springframework.context.support)

  10. run:929, AbstractApplicationContext$2 (org.springframework.context.support)

通过源码分析,原来是通过Spring注册的 ShutdownHook来执行的


  1. @Override

  2. public void registerShutdownHook() {

  3.    if (this.shutdownHook == null) {

  4.        // No shutdown hook registered yet.

  5.        this.shutdownHook = new Thread() {

  6.            @Override

  7.            public void run() {

  8.                synchronized (startupShutdownMonitor) {

  9.                    doClose();

  10.                }

  11.            }

  12.        };

  13.        Runtime.getRuntime().addShutdownHook(this.shutdownHook);

  14.    }

  15. }

通过查阅Java的API文档[2], 我们可以知道ShutdownHook将在下面两种情况下执行

The Java virtual machine shuts down in response to two kinds of events:

  • The program exits normally, when the last non-daemon thread exits or when the exit(equivalently, System.exit) method is invoked, or

  • The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.

  1. 调用了System.exit()方法

  2. 响应外部的信号,例如Ctrl+C(其实发送的是SIGINT信号),或者是 SIGTERM信号(默认 kill $PID发送的是 SIGTERM信号)

因此,正常的应用在停止过程中( kill-9$PID除外),都会执行上述ShutdownHook,它的作用不仅仅是关闭tomcat,还有进行其他的清理工作,在此不再赘述。

总结

  1. 在 DubboConsumer启动的过程中,通过启动一个独立的非daemon线程循环检查变量的状态,确保进程不退出

  2. 在 DubboConsumer停止的过程中,通过执行spring容器的shutdownhook,修改了变量的状态,使得程序正常退出

问题

在DubboProvider的例子中,我们看到Provider并没有启动Tomcat提供HTTP服务,那又是如何实现不退出的呢?我们将在下一篇文章中回答这个问题。

彩蛋

IntellijIDEA中运行了如下的单元测试,创建一个线程执行睡眠1000秒的操作,我们惊奇的发现,代码并没有线程执行完就退出了,这又是为什么呢?(被创建的线程是非daemon线程)


  1. @Test

  2. public void test() {

  3.    new Thread(new Runnable() {

  4.        @Override

  5.        public void run() {

  6.            try {

  7.                Thread.sleep(1000000);

  8.            } catch (InterruptedException e) {

  9.                e.printStackTrace();

  10.            }

  11.        }

  12.    }).start();

  13. }

[1] https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.8

[2] https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#addShutdownHook

关注Java技术栈微信公众号,在后台回复关键字:dubbo,可以获取更多栈长整理的 Dubbo 技术干货。

最近干货分享

厉害了,Dubbo 正式毕业!

46张PPT弄懂JVM、GC算法和性能调优!

Intellij IDEA 解决撸码最头大的问题。

微服务配置中心对比,哪个更牛逼?

分享一份Java架构师学习资料

点击「阅读原文」一起搞技术,爽~

Spring Boot Dubbo 应用启停源码分析的更多相关文章

  1. 涨姿势:Spring Boot 2.x 启动全过程源码分析

    目录 SpringApplication 实例 run 方法运行过程 总结 上篇<Spring Boot 2.x 启动全过程源码分析(一)入口类剖析>我们分析了 Spring Boot 入 ...

  2. Spring Boot REST(二)源码分析

    Spring Boot REST(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) SpringBoot RE ...

  3. Spring Boot 2.x 启动全过程源码分析

    Spring Boot 2.x 启动全过程源码分析 SpringApplication 实例 run 方法运行过程 上面分析了 SpringApplication 实例对象构造方法初始化过程,下面继续 ...

  4. Spring Boot 2.x 启动全过程源码分析(上)入口类剖析

    Spring Boot 的应用教程我们已经分享过很多了,今天来通过源码来分析下它的启动过程,探究下 Spring Boot 为什么这么简便的奥秘. 本篇基于 Spring Boot 2.0.3 版本进 ...

  5. Spring 循环引用(二)源码分析

    Spring 循环引用(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环引用相关文章: & ...

  6. Spring第四天,BeanPostProcessor源码分析,彻底搞懂IOC注入及注解优先级问题!

  7. Spring注解之@Lazy注解,源码分析和总结

    一 关于延迟加载的问题,有次和大神讨论他会不会直接或间接影响其他类.spring的好处就是文档都在代码里,网上百度大多是无用功. 不如,直接看源码.所以把当时源码分析的思路丢上来一波. 二 源码分析 ...

  8. Spring系列28:@Transactional事务源码分析

    本文内容 @Transactional事务使用 @EnableTransactionManagement 详解 @Transactional事务属性的解析 TransactionInterceptor ...

  9. 深入理解Spring之九:DispatcherServlet初始化源码分析

    转载 https://mp.weixin.qq.com/s/UF9s52CBzEDmD0bwMfFw9A DispatcherServlet是SpringMVC的核心分发器,它实现了请求分发,是处理请 ...

随机推荐

  1. js基本函数和基本方法

    日期时间函数(需要用变量调用): var b = new Date(); //获取当前时间 b.getTime() //获取时间戳 b.getFullYear() //获取年份 b.getMonth( ...

  2. Linux的各个发行版本(一)

    三大流派 1.Slackware SUSE Linux Enterprise Server (SLES) OpenSuse桌面 2.debian 迄今为止最遵循GNU规范的Linux系统 Ubuntu ...

  3. MS14-068利用

    漏洞原理详情后续补上:kerberos 协议实现过程中的某些 bug,致使普通域用户可以任意伪造高权限 PAC,去请求 TGS 从而导致的权限提升,漏洞现在很少遇到了. 一.利用 需要拥有一个域账号的 ...

  4. Linux主机名的修改以及配置

    查询主机名: [root@localhost ~]# hostnamelocalhost.localdomain [root@localhost ~]# vim /etc/sysconfig/netw ...

  5. (好题)2017-2018 ACM-ICPC, Asia Tsukuba Regional Contest F Pizza Delivery

    题意:给n个点m条边的有向图.每次使一条边反向,问你1到2的最短路变短,变长,还是不变. 解法:遇到这种题容易想到正向求一遍最短路d1,反向再求一遍最短路d2.纪录原图上的最短路为ans,然后分开考虑 ...

  6. C++如何阻止一个类被实例化

    (1)定义一个无用的抽象函数,使得类成为抽象类 (2)将构造函数定义为private. 为什么要这样做? 一些工具类,没有被实例化的必要.

  7. php自动生成不重复的id

    PHP uniqid()函数可用于生成不重复的唯一标识符,该函数基于微秒级当前时间戳.在高并发或者间隔时长极短(如循环代码)的情况下,会出现大量重复数据.即使使用了第二个参数,也会重复,最好的方案是结 ...

  8. linux安装zabbix的tar包和另外一个并存

    在安装zabbix客户端的时候,发现存在一个zabbix客户端,现在我们要重新建一个来与之并存 第一步安装: cd /data0/software/ tar xf zabbix-3.0.28.tar. ...

  9. Hadoop(二)HDFS

    海量数据处理 分而治之 核心思想: 把数据分发到多个节点 移动计算到数据附近 计算节点进行本地数据处理 优选顺序,次之随机读 一.HDFS概述 修改,先删除,再重新生成 1.架构 namenode维护 ...

  10. Thread.Join理解

    Thread.Join:Blocks the calling thread until a thread terminates MainThread里面起了一个SubThread,从SubThread ...