《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》

摘要: 原创出处 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 )。

img

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

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

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

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

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

img

前面的文章里分析了 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 了,原理?看懂了上面的文章,这个应该不难。

文章目录