作者:张乎兴
来源: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. fedora 26

    图标文件路径: /home/xiezhiyan/.local/share/applications

  2. 马士兵对话京东T6阿里P7(薪水):月薪5万,他为何要离职?

    马士兵大佬你知道吗? 你竟然不知道?你怎么可能不知道!你不知道是不可能的! 记得自己的第一行Java代码,你的Hello World是跟着谁学的吗?我的就是马士兵老师! 马士兵是唯一一个在当时讲课是让 ...

  3. 使用IntelliJ IDEA配置Maven(详细操作)

    一,下载Maven 进入官网http://maven.apache.org/  点击Download 找到如下图所示的区域,注意你的操作系统. 点击安装你所需要的安装包,下载,解压. 二,Maven环 ...

  4. 如何安装python运行环境Anaconda

    参考视频:https://v.qq.com/x/page/u05499rig9s.html

  5. Excel_PoweQuery——条件计数、条件求和

    岁月不居,时节如流. 时光荏苒,岁月如梭. 前面两段充分体现了博主深厚的文学素养,别和博主争,博主说啥就是啥. 其实,对于大量数据的处理,这几年微软Office做的不单单是2007的时候把Excel的 ...

  6. python tkinter 实现 带界面(GUI)的RSA加密、签名

    代码环境,python3.5.2 RSA加密的过程是:使用公钥加密,私钥解密 RSA签名的过程是:使用私钥签名,公钥验证 所以核心代码就是,生成公钥私钥,使用公钥私钥分别进行加密解密. 在实际编码的时 ...

  7. Cas 4.2.7 OAuth+Rest 实现SSO

    关于Cas的认证原理.Rest的使用请参考前面的文章.本文重点阐述使用Rest接口登陆系统和其他单点登录系统打通遇到的问题,及解决问题的思路和过程.    一: 遇到的问题         使用Res ...

  8. 翻译 What is the concept of Service Container in Laravel?

    原文链接: https://stackoverflow.com/questions/37038830/what-is-the-concept-of-service-container-in-larav ...

  9. fragment中的onCreateView和onViewCreated的区别和

    (1)  onViewCreated在onCreateView执行完后立即执行. (2)  onCreateView返回的就是fragment要显示的view.

  10. mysql导入.csv文件出错

    1.报错信息 ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cann ...