做者:张乎兴 来源:Dubbo官方博客
Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在Spring Boot应用场景的开发。同时也整合了 Spring Boot 特性:html
你有没有想过一个问题? incubator-dubbo-spring-boot-project
中的 DubboConsumerDemo
应用就一行代码, main
方法执行完以后,为何不会直接退出呢?java
@SpringBootApplication(scanBasePackages = "com.alibaba.boot.dubbo.demo.consumer.controller") public class DubboConsumerDemo { public static void main(String[] args) { SpringApplication.run(DubboConsumerDemo.class,args); } }
其实要回答这样一个问题,咱们首先须要把这个问题进行一个抽象,即一个JVM进程,在什么状况下会退出?面试
以Java 8为例,经过查阅JVM语言规范[1],在12.8章节中有清晰的描述:spring
A program terminates all its activity and exits when one of two things happens:apache
exit
method of class Runtime
or class System
, and the exit
operation is not forbidden by the security manager.也就是说,致使JVM的退出只有2种状况:bootstrap
System.exit()
或 Runtime.exit()
所以针对上面的状况,咱们判断,必定是有某个非daemon线程没有退出致使。咱们知道,经过jstack能够看到全部的线程信息,包括他们是不是daemon线程,能够经过jstack找出那些是非deamon的线程。c#
jstack 57785 | grep tid | grep -v "daemon" "container-0" #37 prio=5 os_prio=31 tid=0x00007fbe312f5800 nid=0x7103 waiting on condition [0x0000700010144000] "container-1" #49 prio=5 os_prio=31 tid=0x00007fbe3117f800 nid=0x7b03 waiting on condition [0x0000700010859000] "DestroyJavaVM" #83 prio=5 os_prio=31 tid=0x00007fbe30011000 nid=0x2703 waiting on condition [0x0000000000000000] "VM Thread" os_prio=31 tid=0x00007fbe3005e800 nid=0x3703 runnable "GC Thread#0" os_prio=31 tid=0x00007fbe30013800 nid=0x5403 runnable "GC Thread#1" os_prio=31 tid=0x00007fbe30021000 nid=0x5303 runnable "GC Thread#2" os_prio=31 tid=0x00007fbe30021800 nid=0x2d03 runnable "GC Thread#3" os_prio=31 tid=0x00007fbe30022000 nid=0x2f03 runnable "G1 Main Marker" os_prio=31 tid=0x00007fbe30040800 nid=0x5203 runnable "G1 Conc#0" os_prio=31 tid=0x00007fbe30041000 nid=0x4f03 runnable "G1 Refine#0" os_prio=31 tid=0x00007fbe31044800 nid=0x4e03 runnable "G1 Refine#1" os_prio=31 tid=0x00007fbe31045800 nid=0x4d03 runnable "G1 Refine#2" os_prio=31 tid=0x00007fbe31046000 nid=0x4c03 runnable "G1 Refine#3" os_prio=31 tid=0x00007fbe31047000 nid=0x4b03 runnable "G1 Young RemSet Sampling" os_prio=31 tid=0x00007fbe31047800 nid=0x3603 runnable "VM Periodic Task Thread" os_prio=31 tid=0x00007fbe31129000 nid=0x6703 waiting on condition
此处经过grep tid 找出全部的线程摘要,经过grep -v找出不包含daemon关键字的行
经过上面的结果,咱们发现了一些信息:后端
container-0
, container-1
很是可疑,他们是非daemon线程,处于wait状态综上,咱们能够推断,极可能是由于 container-0
和 container-1
致使JVM没有退出。如今咱们经过源码,搜索一下究竟是谁建立的这两个线程。api
经过对spring-boot的源码分析,咱们在 org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer
的 startDaemonAwaitThread
找到了以下代码tomcat
private void startDaemonAwaitThread() { Thread awaitThread = new Thread("container-" + (containerCounter.get())) { @Override public void run() { TomcatEmbeddedServletContainer.this.tomcat.getServer().await(); } }; awaitThread.setContextClassLoader(getClass().getClassLoader()); awaitThread.setDaemon(false); awaitThread.start(); }
在这个方法加个断点,看下调用堆栈:
initialize:115, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat) <init>:84, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat) getTomcatEmbeddedServletContainer:554, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat) getEmbeddedServletContainer:179, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat) createEmbeddedServletContainer:164, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded) onRefresh:134, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded) refresh:537, AbstractApplicationContext (org.springframework.context.support) refresh:122, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded) refresh:693, SpringApplication (org.springframework.boot) refreshContext:360, SpringApplication (org.springframework.boot) run:303, SpringApplication (org.springframework.boot) run:1118, SpringApplication (org.springframework.boot) run:1107, SpringApplication (org.springframework.boot) 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()
这个方法中,线程是如何实现不退出的。这里为了阅读方便,去掉了不相关的代码。
public void await() { // ... if( port==-1 ) { try { awaitThread = Thread.currentThread(); while(!stopAwait) { try { Thread.sleep( 10000 ); } catch( InterruptedException ex ) { // continue and check the flag } } } finally { awaitThread = null; } return; } // ... }
在await方法中,实际上当前线程在一个while循环中每10秒检查一次 stopAwait
这个变量,它是一个 [volatile](http://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247483916&idx=1&sn=89daf388da0d6fe40dc54e9a4018baeb&chksm=eb53873adc240e2cf55400f3261228d08fc943c4f196566e995681549c47630b70ac01b75031&scene=21#wechat_redirect)
类型变量,用于确保被另外一个线程修改后,当前线程可以当即看到这个变化。若是没有变化,就会一直处于while循环中。这就是该线程不退出的缘由,也就是整个spring-boot应用不退出的缘由。
由于Springboot应用同时启动了8080和8081(management port)两个端口,实际是启动了两个Tomcat,所以会有两个线程 container-0
和 container-1
。
接下来,咱们再看看,这个Spring-boot应用又是如何退出的呢?
在前面的描述中提到,有一个线程持续的在检查 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
的线程调用了该方法:
stopAwait:390, StandardServer (org.apache.catalina.core) stopInternal:819, StandardServer (org.apache.catalina.core) stop:226, LifecycleBase (org.apache.catalina.util) stop:377, Tomcat (org.apache.catalina.startup) stopTomcat:241, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat) stop:295, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat) stopAndReleaseEmbeddedServletContainer:306, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded) onClose:155, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded) doClose:1014, AbstractApplicationContext (org.springframework.context.support) run:929, AbstractApplicationContext$2 (org.springframework.context.support)
经过源码分析,原来是经过Spring注册的 ShutdownHook
来执行的
@Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.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, 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.
SIGTERM
信号(默认 kill $PID
发送的是 SIGTERM
信号)所以,正常的应用在中止过程当中( kill-9$PID
除外),都会执行上述ShutdownHook,它的做用不只仅是关闭tomcat,还有进行其余的清理工做,在此再也不赘述。
DubboConsumer
启动的过程当中,经过启动一个独立的非daemon线程循环检查变量的状态,确保进程不退出DubboConsumer
中止的过程当中,经过执行spring容器的shutdownhook,修改了变量的状态,使得程序正常退出在DubboProvider的例子中,咱们看到Provider并无启动Tomcat提供HTTP服务,那又是如何实现不退出的呢?咱们将在下一篇文章中回答这个问题。
在 IntellijIDEA
中运行了以下的单元测试,建立一个线程执行睡眠1000秒的操做,咱们惊奇的发现,代码并无线程执行完就退出了,这又是为何呢?(被建立的线程是非daemon线程)
@Test public void test() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
[1] https://docs.oracle.com/javas...
[2] https://docs.oracle.com/javas...
关注Java技术栈微信公众号,在后台回复关键字:dubbo,能够获取更多栈长整理的 Dubbo 技术干货。
推荐去个人博客阅读更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
以为不错,别忘了点赞+转发哦!