⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://juejin.im/post/5a6d77916fb9a01c9c1f4440 「预流」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

之前的几篇文章讲了 Tomcat 的启动过程,在默认的配置下启动完之后会看到后台实际上总共有 6 个线程在运行。即 1 个用户线程,剩下 5 个为守护线程(下图中的 Daemon Thread )。

如果对什么叫守护线程的概念比较陌生,这里再重复一下:

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。这种线程并不属于程序中不可或缺的部分,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon(true) 方法来实现。

Tomcat 的关闭正是利用了这个原理,即只要将那唯一的一个用户线程关闭,则整个应用就关闭了。

要研究这个用户线程怎么被关闭的得先从这个线程从何产生说起。在前面分析 Tomcat 的启动时我们是从org.apache.catalina.startup.Bootstrap类的 main 方法作为入口,该类的 453 到 456 行是 Tomcat 启动时会执行的代码:

前面的文章里分析了 daemon.load 和 daemon.start 方法,这里请注意

daemon.setAwait(true);

这句,它的作用是通过反射调用

org.apache.catalina.startup.Catalina

类的 setAwait(true) 方法,最终将 Catalina 类的实例变量 await 设值为 true 。

Catalina 类的 setAwait 方法代码:

/**
* Set flag.
*/
public void setAwait(boolean await)
throws Exception {

Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Boolean.TYPE;
Object paramValues[] = new Object[1];
paramValues[0] = Boolean.valueOf(await);
Method method =
catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
method.invoke(catalinaDaemon, paramValues);

}

如前文分析,Tomcat 启动时会调用org.apache.catalina.startup.Catalina类的 start 方法,看下这个方法的代码:

 1	    /**
2 * Start a new server instance.
3 */
4 public void start() {
5
6 if (getServer() == null) {
7 load();
8 }
9
10 if (getServer() == null) {
11 log.fatal("Cannot start server. Server instance is not configured.");
12 return;
13 }
14
15 long t1 = System.nanoTime();
16
17 // Start the new server
18 try {
19 getServer().start();
20 } catch (LifecycleException e) {
21 log.fatal(sm.getString("catalina.serverStartFail"), e);
22 try {
23 getServer().destroy();
24 } catch (LifecycleException e1) {
25 log.debug("destroy() failed for failed Server ", e1);
26 }
27 return;
28 }
29
30 long t2 = System.nanoTime();
31 if(log.isInfoEnabled()) {
32 log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
33 }
34
35 // Register shutdown hook
36 if (useShutdownHook) {
37 if (shutdownHook == null) {
38 shutdownHook = new CatalinaShutdownHook();
39 }
40 Runtime.getRuntime().addShutdownHook(shutdownHook);
41
42 // If JULI is being used, disable JULI's shutdown hook since
43 // shutdown hooks run in parallel and log messages may be lost
44 // if JULI's hook completes before the CatalinaShutdownHook()
45 LogManager logManager = LogManager.getLogManager();
46 if (logManager instanceof ClassLoaderLogManager) {
47 ((ClassLoaderLogManager) logManager).setUseShutdownHook(
48 false);
49 }
50 }
51
52 if (await) {
53 await();
54 stop();
55 }
56 }

前文分析启动时发现通过第 19 行 getServer().start() 的这次方法调用,Tomcat 接下来会一步步启动所有在配置文件中配置的组件。后面的代码没有分析,这里请关注最后第 52 到 55 行,上面说到已经将 Catalina 类的实例变量 await 设值为 true,所以这里将会执行 Catalina 类的 await 方法:

/**
* Await and shutdown.
*/
public void await() {

getServer().await();

}

该方法就一句话,意思是调用org.apache.catalina.core.StandardServer类的 await 方法:

  1	    /**
2 * Wait until a proper shutdown command is received, then return.
3 * This keeps the main thread alive - the thread pool listening for http
4 * connections is daemon threads.
5 */
6 @Override
7 public void await() {
8 // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
9 if( port == -2 ) {
10 // undocumented yet - for embedding apps that are around, alive.
11 return;
12 }
13 if( port==-1 ) {
14 try {
15 awaitThread = Thread.currentThread();
16 while(!stopAwait) {
17 try {
18 Thread.sleep( 10000 );
19 } catch( InterruptedException ex ) {
20 // continue and check the flag
21 }
22 }
23 } finally {
24 awaitThread = null;
25 }
26 return;
27 }
28
29 // Set up a server socket to wait on
30 try {
31 awaitSocket = new ServerSocket(port, 1,
32 InetAddress.getByName(address));
33 } catch (IOException e) {
34 log.error("StandardServer.await: create[" + address
35 + ":" + port
36 + "]: ", e);
37 return;
38 }
39
40 try {
41 awaitThread = Thread.currentThread();
42
43 // Loop waiting for a connection and a valid command
44 while (!stopAwait) {
45 ServerSocket serverSocket = awaitSocket;
46 if (serverSocket == null) {
47 break;
48 }
49
50 // Wait for the next connection
51 Socket socket = null;
52 StringBuilder command = new StringBuilder();
53 try {
54 InputStream stream;
55 try {
56 socket = serverSocket.accept();
57 socket.setSoTimeout(10 * 1000); // Ten seconds
58 stream = socket.getInputStream();
59 } catch (AccessControlException ace) {
60 log.warn("StandardServer.accept security exception: "
61 + ace.getMessage(), ace);
62 continue;
63 } catch (IOException e) {
64 if (stopAwait) {
65 // Wait was aborted with socket.close()
66 break;
67 }
68 log.error("StandardServer.await: accept: ", e);
69 break;
70 }
71
72 // Read a set of characters from the socket
73 int expected = 1024; // Cut off to avoid DoS attack
74 while (expected < shutdown.length()) {
75 if (random == null)
76 random = new Random();
77 expected += (random.nextInt() % 1024);
78 }
79 while (expected > 0) {
80 int ch = -1;
81 try {
82 ch = stream.read();
83 } catch (IOException e) {
84 log.warn("StandardServer.await: read: ", e);
85 ch = -1;
86 }
87 if (ch < 32) // Control character or EOF terminates loop
88 break;
89 command.append((char) ch);
90 expected--;
91 }
92 } finally {
93 // Close the socket now that we are done with it
94 try {
95 if (socket != null) {
96 socket.close();
97 }
98 } catch (IOException e) {
99 // Ignore
100 }
101 }
102
103 // Match against our command string
104 boolean match = command.toString().equals(shutdown);
105 if (match) {
106 log.info(sm.getString("standardServer.shutdownViaPort"));
107 break;
108 } else
109 log.warn("StandardServer.await: Invalid command '"
110 + command.toString() + "' received");
111 }
112 } finally {
113 ServerSocket serverSocket = awaitSocket;
114 awaitThread = null;
115 awaitSocket = null;
116
117 // Close the server socket and return
118 if (serverSocket != null) {
119 try {
120 serverSocket.close();
121 } catch (IOException e) {
122 // Ignore
123 }
124 }
125 }
126 }

这段代码就不一一分析,总体作用如方法前的注释所说,即“一直等待到接收到一个正确的关闭命令后该方法将会返回。这样会使主线程一直存活——监听http连接的线程池是守护线程”。

熟悉 Java 的 Socket 编程的话对这段代码就很容易理解,就是默认地址(地址值由实例变量 address 定义,默认为localhost)的默认的端口(端口值由实例变量 port 定义,默认为8005)上监听 Socket 连接,当发现监听到的连接的输入流中的内容与默认配置的值匹配(该值默认为字符串SHUTDOWN)则跳出循环,该方法返回(第 103 到 107 行)。否则该方法会一直循环执行下去。 一般来说该用户主线程会阻塞(第 56 行)直到有访问localhost:8005的连接出现。 正因为如此才出现开头看见的主线程一直 Running 的情况,而因为这个线程一直 Running ,其它守护线程也会一直存在。

说完这个线程的产生,接下来看看这个线程的关闭,按照上面的分析,这个线程提供了一个关闭机制,即只要访问localhost:8005,并且发送一个内容为SHUTDOWN的字符串,就可以关闭它了。

Tomcat 正是这么做的,一般来说关闭 Tomcat 通过执行 shutdown.bat 或 shutdown.sh 脚本,关于这段脚本可参照分析启动脚本那篇文章,机制类似,最终会执行org.apache.catalina.startup.Bootstrap类的 main 方法,并传入入参"stop",看下本文第 2 张图片中org.apache.catalina.startup.Bootstrap类的第 458 行,接着将调用org.apache.catalina.startup.Catalina类 stopServer 方法:

 1	    public void stopServer(String[] arguments) {
2
3 if (arguments != null) {
4 arguments(arguments);
5 }
6
7 Server s = getServer();
8 if( s == null ) {
9 // Create and execute our Digester
10 Digester digester = createStopDigester();
11 digester.setClassLoader(Thread.currentThread().getContextClassLoader());
12 File file = configFile();
13 FileInputStream fis = null;
14 try {
15 InputSource is =
16 new InputSource(file.toURI().toURL().toString());
17 fis = new FileInputStream(file);
18 is.setByteStream(fis);
19 digester.push(this);
20 digester.parse(is);
21 } catch (Exception e) {
22 log.error("Catalina.stop: ", e);
23 System.exit(1);
24 } finally {
25 if (fis != null) {
26 try {
27 fis.close();
28 } catch (IOException e) {
29 // Ignore
30 }
31 }
32 }
33 } else {
34 // Server object already present. Must be running as a service
35 try {
36 s.stop();
37 } catch (LifecycleException e) {
38 log.error("Catalina.stop: ", e);
39 }
40 return;
41 }
42
43 // Stop the existing server
44 s = getServer();
45 if (s.getPort()>0) {
46 Socket socket = null;
47 OutputStream stream = null;
48 try {
49 socket = new Socket(s.getAddress(), s.getPort());
50 stream = socket.getOutputStream();
51 String shutdown = s.getShutdown();
52 for (int i = 0; i < shutdown.length(); i++) {
53 stream.write(shutdown.charAt(i));
54 }
55 stream.flush();
56 } catch (ConnectException ce) {
57 log.error(sm.getString("catalina.stopServer.connectException",
58 s.getAddress(),
59 String.valueOf(s.getPort())));
60 log.error("Catalina.stop: ", ce);
61 System.exit(1);
62 } catch (IOException e) {
63 log.error("Catalina.stop: ", e);
64 System.exit(1);
65 } finally {
66 if (stream != null) {
67 try {
68 stream.close();
69 } catch (IOException e) {
70 // Ignore
71 }
72 }
73 if (socket != null) {
74 try {
75 socket.close();
76 } catch (IOException e) {
77 // Ignore
78 }
79 }
80 }
81 } else {
82 log.error(sm.getString("catalina.stopServer"));
83 System.exit(1);
84 }
85 }

第 8 到 41 行是读取配置文件,可参照前面分析 Digester 的文章,不再赘述。从第 49 行开始,即向localhost:8005发起一个 Socket 连接,并写入SHUTDOWN字符串。 这样将会关闭 Tomcat 中的那唯一的一个用户线程,接着所有守护线程将会退出(由 JVM 保证),之后整个应用关闭。

以上分析 Tomcat 的默认关闭机制,但这是通过运行脚本来关闭,我觉得这样比较麻烦,那么能不能通过一种在线访问的方式关闭 Tomcat 呢?当然可以,比较暴力的玩法是直接改org.apache.catalina.core.StandardServer的源码第 500 行,将

boolean match = command.toString().equals(shutdown);

改成

boolean match = command.toString().equals(“GET /SHUTDOWN HTTP/1.1”);

或者修改 server.xml 文件,找到 Server 节点,将原来的

<Server port="8005" shutdown="SHUTDOWN">

改成

<Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1">

这样直接在浏览器中输入http://localhost:8005/SHUTDOWN就可以关闭 Tomcat 了,原理?看懂了上面的文章,这个应该不难。

文章目录