背景介绍
Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在Spring Boot应用场景的开发。同时也整合了 Spring Boot 特性:
1、自动装配 (比如: 注解驱动, 自动装配等).
2、Production-Ready (比如: 安全, 健康检查, 外部化配置等).
DubboConsumer启动分析
你有没有想过一个问题?incubator-dubbo-spring-boot-project中的DubboConsumerDemo应用就一行代码,main方法执行完之后,为什么不会直接退出呢?
其实要回答这样一个问题,我们首先需要把这个问题进行一个抽象,即一个JVM进程,在什么情况下会退出?
以Java 8为例,通过查阅JVM语言规范[1],在12.8章节中有清晰的描述:
A program terminates all its activity and exits when one of two things happens:
1、All the threads that are not daemon threads terminate.
2、Some thread invokes the exit method of class Runtime or class System, and the exit operation is not forbidden by the security manager.
也就是说,导致JVM的退出只有2种情况:
1、所有的非daemon进程完全终止
2、某个线程调用了System.exit()或Runtime.exit()
因此针对上面的情况,我们判断,一定是有某个非daemon线程没有退出导致。我们知道,通过jstack可以看到所有的线程信息,包括他们是否是daemon线程,可以通过jstack找出那些是非deamon的线程。
此处通过grep tid 找出所有的线程摘要,通过grep -v找出不包含daemon关键字的行通过上面的结果,我们发现了一些信息:
1、有两个线程container-0, container-1非常可疑,他们是非daemon线程,处于wait状态
2、有一些GC相关的线程,和VM打头的线程,也是非daemon线程,但他们很有可能是JVM自己的线程,在此暂时忽略。
综上,我们可以推断,很可能是因为container-0和container-1导致JVM没有退出。现在我们通过源码,搜索一下到底是谁创建的这两个线程。
通过对spring-boot的源码分析,我们在org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer的startDaemonAwaitThread找到了如下代码
在这个方法加个断点,看下调用堆栈:
可以看到,spring-boot应用在启动的过程中,由于默认启动了Tomcat暴露HTTP服务,所以执行到了上述方法,而Tomcat启动的所有的线程,默认都是daemon线程,例如监听请求的Acceptor,工作线程池等等,如果这里不加控制的话,启动完成之后JVM也会退出。因此需要显示的启动一个线程,在某个条件下进行持续等待,从而避免线程退出。
下面我们在深挖一下,在Tomcat的this.tomcat.getServer().await()这个方法中,线程是如何实现不退出的。这里为了阅读方便,去掉了不相关的代码。
在await方法中,实际上当前线程在一个while循环中每10秒检查一次 stopAwait这个变量,它是一个volatile类型变量,用于确保被另一个线程修改后,当前线程能够立即看到这个变化。如果没有变化,就会一直处于while循环中。这就是该线程不退出的原因,也就是整个spring-boot应用不退出的原因。
因为Springboot应用同时启动了8080和8081(management port)两个端口,实际是启动了两个Tomcat,因此会有两个线程container-0和container-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
可以看到有一个名为Thread-3的线程调用了该方法:
通过源码分析,原来是通过Spring注册的ShutdownHook来执行的
通过查阅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, orThe 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.
调用了System.exit()方法
响应外部的信号,例如Ctrl+C(其实发送的是SIGINT信号),或者是SIGTERM信号(默认kill $PID发送的是SIGTERM信号)
因此,正常的应用在停止过程中(kill -9 $PID除外),都会执行上述ShutdownHook,它的作用不仅仅是关闭tomcat,还有进行其他的清理工作,在此不再赘述。
总结
在DubboConsumer启动的过程中,通过启动一个独立的非daemon线程循环检查变量的状态,确保进程不退出
在DubboConsumer停止的过程中,通过执行spring容器的shutdownhook,修改了变量的状态,使得程序正常退出
问题
在DubboProvider的例子中,我们看到Provider并没有启动Tomcat提供HTTP服务,那又是如何实现不退出的呢?我们将在下一篇文章中回答这个问题。
彩蛋
在Intellij IDEA中运行了如下的单元测试,创建一个线程执行睡眠1000秒的操作,我们惊奇的发现,代码并没有线程执行完就退出了,这又是为什么呢?(被创建的线程是非daemon线程)
[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
本文作者:中间件小哥
本文为云栖社区原创内容,未经允许不得转载。
网友评论