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主线程的正常退出。
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
映射:
<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”>
test tomcat SHUTDOWN
test tomcat SHUTDOWN
UTF-8
TestShutdownServlet