vlambda博客
学习文章列表

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

本篇约2.9千字,阅读时长约5分钟。周末水文,但还是有些心得想与你分享,希望对你用。

  • 一、前言

    • 守护线程守护了谁

  • 二、Tomcat主线程监听SHUTDOWN

    • 源码展示

  • 三、模拟Tomcat远程关闭

    • 1、正常远程关闭

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

  • 四、经验总结

This browser does not support music or audio playback. Please play it in Weixin or another browser.

我当以生于我华夏

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

一、前言

本来不想写这篇文章的,因为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?守护线程守护了谁?
await A valid shutdown command

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?守护线程守护了谁?
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?守护线程守护了谁?
非守护用户线程下远程关闭Tomcat

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

Tomcat没有正常退出

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

四、经验总结

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

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

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

如若文章有错误理解,欢迎批评指正,同时非常期待你的留言和点赞。
如果觉得有用,不妨点个在看,让更多人受益。