文章目录
- 一、前言
- 守护线程守护了谁
- 二、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
指令,进而调用了一些停止销毁操作。
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
下即可:
在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的方式,在实际生产中可能并不常见,至少我现在的公司web项目没有这样做。更常见的是ps
查看Tomcat进程id,然后直接kill
,没必要还写个socket
客户端,发送SHUTDOWN
给Tomcat,但也是多了一种选择。其实ps+kill
的方式也不简单,这是两个操作,如果需要实现远程关闭,还要需要免密的ssh远程传输命令。
此次文章内容较为简单,但是至少对守护线程有了一个更深的认识,不止停留于概念。在一些开源组件中看到创建线程也一般都是会设置为守护线程,我觉得这是一种规范吧。
Tomcat源码详细注释链接(非推广,持续更新):https://gitee.com/stefanpy/tomcat-source-code-learning
如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。