文章目录


  • 一、前言
  • 守护线程守护了谁
  • 二、Tomcat主线程监听SHUTDOWN
  • 源码展示
  • 三、模拟Tomcat远程关闭
  • 1、正常远程关闭
  • 2、非守护用户线程下远程关闭
  • 四、经验总结


一、前言

本来不想写这篇文章的,因为​​Tomcat​​​监听​​SHUTDOWN​​的原理实在太简单,前面几篇Tomcat系列文章也提到过。但是,昨天上班的时候写了一个定时任务,放在公司的分布式任务调度平台上,定了每2分钟运行一次,开开心心点击启动,然后就去做别的事了。

过了一段时间,打开调度平台,查看调度情况,我惊呆了,一长溜显示调度成功,但还在运行的调度。我寻思难道任务执行器回调出问题了?心跳检测了一下挺正常;难道是我的定时任务很耗时,超过了2分钟,都排队运行?不应该呀,就扫描更新个数据,而且本地测试数据很少的。于是制造了一些假数据,复制任务命令到机器上运行,日志打印显示成功,耗时4秒多,但迟迟不退出。

经过郁闷+打堆栈,发现有一个线程循环运行导致主线程不能正常退出,后来查到是业务代码里调用了同事写的一个组件,而这个组件里会起一个定时线程,还是个非守护线程,才导致我的任务主线程运行完不能正常退出。

原因找到了,解决方法也很简单,就是在任务代码里也新建一个Thread,并设置成守护线程,在这个守护线程里调用同事写的组件,用Future同步获取结果。这样组件里的定时线程也是守护线程了,主线程就可以正常退出了。

守护线程守护了谁

守护线程的概念我知道呀,线程分为两种,一种是非守护线程,一种是守护线程,当所有的非守护线程退出后,守护线程因为没有了守护对象也就跟着结束了。需要注意:


  • 创建线程时如果没有明确​​setDaemon(true)​​,一般默认是非守护线程。
  • 守护线程里创建的子线程默认也是守护线程,除非​​setDaemon(false)​​。

二、Tomcat主线程监听SHUTDOWN

Tomcat中的线程也有两种,主线程是非守护线程,其他单独创建的线程或者线程池创建的工作线程都默认是守护线程。

当Tomcat启动时,做完所有初始化和启动工作,主线程会进入一个无限循环监听默认8005端口的状态,直到网络中读取到​​SHUTDOWN​​​指令,才会退出循环,进而调用​​Tomcat​​停止销毁操作。

源码展示

​org.apache.catalina.startup.Catalina#start​

public void start() {
// 省略调用Server init、start等操作
if (await) {
// 调用Server的await,循环等待shutdown指令
await();
// 如果接收到shutdown,就结束await(),调用stop停止Tomcat
stop();
}
}

源码很简单,建立一个​​ServerSocket​​​,循环监听读取网络中是否有​​SHUTDOWN​​指令传来:

public void await() {
// 省略部分无关紧要代码
// Set up a server socket to wait on
try {
// 建立一个server socket 端口默认为8005
awaitSocket = new ServerSocket(getPortWithOffset(), 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error(sm.getString("standardServer.awaitSocket.fail", address,
String.valueOf(getPortWithOffset()), String.valueOf(getPort()),
String.valueOf(getPortOffset())), e);
return;
}

try {
awaitThread = Thread.currentThread();
// Loop waiting for a connection and a valid command
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}

// Wait for the next connection
Socket socket = null;
StringBuilder command = new StringBuilder();
try {
InputStream stream;
long acceptStartTime = System.currentTimeMillis();
try {
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
stream = socket.getInputStream();
} catch (SocketTimeoutException ste) {
// This should never happen but bug 56684 suggests that
// it does.
log.warn(sm.getString("standardServer.accept.timeout",
Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
continue;
} catch (AccessControlException ace) {
log.warn(sm.getString("standardServer.accept.security"), ace);
continue;
} catch (IOException e) {
if (stopAwait) {
// Wait was aborted with socket.close()
break;
}
log.error(sm.getString("standardServer.accept.error"), e);
break;
}
// 读取 command
while (expected > 0) {
int ch = -1;
try {
ch = stream.read();
} catch (IOException e) {
log.warn(sm.getString("standardServer.accept.readError"), e);
ch = -1;
}
// Control character or EOF (-1) terminates loop
if (ch < 32 || ch == 127) {
break;
}
command.append((char) ch);
expected--;
}
} finally {
// Close the socket now that we are done with it
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
// Match against our command string
// 匹配shutdown指令
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
// 匹配成功,退出循环
break;
} else
log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
}
} finally {
ServerSocket serverSocket = awaitSocket;
awaitThread = null;
awaitSocket = null;

// Close the server socket and return
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
}
}

Tomcat的​​Connector​​​是在线程池里处理请求连接,可以引用公共的​​Executor​​组件,也可以创建私有的线程池,线程池中创建的工作线程默认都是守护线程,这样web项目里创建的线程,默认也都是守护线程。Tomcat主线程退出,web项目中的用户线程也跟着退出。

如果用户线程被私自设置成非守护线程,或者设置​​Connector​​中的线程池创建的工作线程是非守护的,就会导致用户的非守护线程阻碍Tomcat主线程的正常退出。

三、模拟Tomcat远程关闭

1、正常远程关闭

运行Tomcat,并保持默认​​SHUTDOWN​​​端口8005。Tomcat启动成功后,运行如下代码即可向8005发送​​SHUTDOWN​​指令:

public class TestTomcatShutdown {
public static void main(String[] args) throws InterruptedException {
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 8005);
String shutdown = "SHUTDOWN";
socket.getOutputStream().write(shutdown.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

从Tomcat日志中可以看出,Tomcat收到了一个合法的​​SHUTDOWN​​指令,进而调用了一些停止销毁操作。

Tomcat主线程监听SHUTDOWN,如何远程关闭Tomcat?守护线程守护了谁?_原力计划

2、非守护用户线程下远程关闭

如果用户创建的线程是非守护线程,看看Tomcat收到​​SHUTDOWN​​指令后能否正常退出。简单写一个Servlet并创建一个循环运行的非守护线程,部署到Tomcat中:

package com.stefan;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
public class TestShutdownServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("TestShutdownServlet doGet....");
System.out.println(TestShutdownServlet.class.getClassLoader());
System.out.println(Thread.currentThread().getName());
TestThread testThread = new TestThread();
testThread.setDaemon(false);
testThread.start();
System.out.println("testThread.isDaemon()=" + testThread.isDaemon());
}
}
package com.stefan;

public class TestThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hhhhhhhh");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

将上述两个类编译,并建立一个web目录test,放到​​webapps​​下即可:

Tomcat主线程监听SHUTDOWN,如何远程关闭Tomcat?守护线程守护了谁?_守护线程_02

在​​web.xml​​​里指定​​servlet​​映射:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="true">

<description>
test tomcat SHUTDOWN
</description>
<display-name>test tomcat SHUTDOWN</display-name>

<request-character-encoding>UTF-8</request-character-encoding>
<servlet>
<servlet-name>TestShutdownServlet</servlet-name>
<servlet-class>com.stefan.TestShutdownServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>TestShutdownServlet</servlet-name>
<url-pattern>/testShutdown</url-pattern>
</servlet-mapping>
</web-app>

启动Tomcat会自动部署test项目,访问http://127.0.0.1:8081/test/testShutdown

Tomcat主线程监听SHUTDOWN,如何远程关闭Tomcat?守护线程守护了谁?_Tomcat10.0.6_03

此时再运行远程关闭代码:

Tomcat主线程监听SHUTDOWN,如何远程关闭Tomcat?守护线程守护了谁?_原力计划_04

Tomcat接收到了​​SHUTDOWN​​指令,并做了停止销毁操作,但依然没有退出,也无法对外提供服务。

四、经验总结

这种远程关闭Tomcat的方式,在实际生产中可能并不常见,至少我现在的公司web项目没有这样做。更常见的是​​ps​​​查看Tomcat进程id,然后直接​​kill​​​,没必要还写个​​socket​​​客户端,发送​​SHUTDOWN​​​给Tomcat,但也是多了一种选择。其实​​ps+kill​​的方式也不简单,这是两个操作,如果需要实现远程关闭,还要需要免密的ssh远程传输命令。

此次文章内容较为简单,但是至少对守护线程有了一个更深的认识,不止停留于概念。在一些开源组件中看到创建线程也一般都是会设置为守护线程,我觉得这是一种​规范​吧。

Tomcat源码详细注释链接(非推广,持续更新):https://gitee.com/stefanpy/tomcat-source-code-learning

如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。