在Servlet的destroy()方法中调用System.exit()方法

5

这是我早前的问题的跟进。

Tomcat 5.0.28存在一个bug,即容器在关闭时未调用Servlet的destroy()方法。这在Tomcat 5.0.30中得到了修复,但如果Servlet的destroy()方法有System.exit(),则会导致Tomcat Windows服务抛出错误1053并拒绝正常关闭(请参见上面链接了解更多详细信息)。

是否有人知道:

  • 在Servlet的destroy()方法中调用System.exit()以强制终止任何非守护线程是否明智?

  • 为什么Tomcat 5.0.30和(包括Tomcat 6.x.x在内的)后续版本如果在Servlet的destroy()方法中有System.exit()将无法正确关闭?

4个回答

16
在Servlet的destroy()方法内部调用System.exit()强制杀死任何非守护线程是一个好主意吗?
绝对不是好主意,而是可怕的主意。当Servlet被移出服务时(例如,停止Servlet/Web应用程序、卸载Web应用程序、重启Web应用程序等),就会调用destroy()方法。
System.exit()将关闭整个JVM!为什么您要因为卸载一个Servlet而强制关闭整个服务器呢?
为了防止这种危险行为,Tomcat 5.0.30以及后续版本(包括Tomcat 6.x.x)如果在Servlet的destroy()方法中有System.exit()操作,可能无法正确地关闭。
您不应编写假设您的代码/应用程序是服务器上唯一运行的东西的代码。

好的,我正在维护这段代码。而且很有把握这将是唯一在此Tomcat上运行的网络应用程序。 - Nikhil Kashyap
3
+1,无论在任何情况下,都不应该在servlet中调用System.exit()。结束。 - David Z
@Nikhil - 这并没有解释你为什么要这样做。你问了这是否是一个好主意,答案是这是一个不好的实践。也许如果你能告诉我们你试图达到什么目的,我们可以帮助你找到更好的解决方案? - matt b
换句话说,这个"错误1053"是一种不良实践的副作用,而不是你问题的真正原因。 - matt b
请看下面我的答案,其中包含了一个最佳实践模式,用于管理由Servlet启动的线程。 - Jared

12

你提出了两个问题:

问题1:在Servlet的destroy()方法中调用System.exit()来强制终止任何非守护线程是一个好主意吗?

在任何与servlet相关的方法中调用System.exit()都是100%不正确的。你的代码并不是在JVM中唯一运行的代码,即使你是唯一的servlet运行(servlet容器有资源需要在JVM真正退出时进行清理)。

处理这种情况的正确方法是在destroy()方法中清理你的线程。这意味着以一种让你能够正确地温和地停止它们的方式启动它们。以下是一个示例(其中MyThread是你的一个线程,并且扩展了ServletManagedThread):

 public class MyServlet extends HttpServlet {
    private List<ServletManagedThread> threads = new ArrayList<ServletManagedThread>();     

     // lots of irrelevant stuff left out for brevity

    public void init() {
        ServletManagedThread t = new MyThread();
        threads.add(t);
        t.start();
    }

    public void destroy() {
        for(ServletManagedThread thread : threads) {
           thread.stopExecuting();
        }
    }
 }

 public abstract class ServletManagedThread extends Thread {

    private boolean keepGoing = true;

    protected abstract void doSomeStuff();
    protected abstract void probablySleepForABit();
    protected abstract void cleanup();

    public void stopExecuting() {
       keepRunning = false;
    }

    public void run() {
       while(keepGoing) {
            doSomeStuff();
            probablySleepForABit();
       }
       this.cleanup();
    }
}

值得注意的是,有一些线程/并发库可以帮助处理这个问题 - 但如果您确实在servlet初始化时启动了一些线程,并且应该在servlet被销毁之前一直运行,那么这可能就是你需要的全部。
问题2: 为什么Tomcat 5.0.30及其后续版本(包括Tomcat 6.x.x)在Servlet的destroy()方法中存在System.exit()时无法正确关闭?
没有更多的分析,很难确定。Microsoft说当Windows要求服务关闭但请求超时时,会出现错误1053。这似乎表明Tomcat内部发生了某些严重的问题。我肯定会怀疑您对System.exit()的调用可能是罪魁祸首。Tomcat(特别是Catalina)确实与VM注册了一个shutdown hook(至少在5.0.30中,参见org.apache.catalina.startup.Catalina.start())。当您调用System.exit()时,该关机钩将由JVM调用。关机钩委托给正在运行的服务,因此每个服务都可能需要做很多工作。
如果关机钩(由您的System.exit()触发)无法执行(例如死锁等),则非常容易理解为何会出现错误1053,根据Runtime.exit(int)方法的文档(从System.exit()调用):
如果在虚拟机开始其关闭序列之后调用此方法,则如果正在运行关闭挂钩,则此方法将无限期地阻塞。如果已经运行了关闭挂钩并启用了退出终止,则如果状态为非零,则此方法使用给定的状态代码停止虚拟机;否则,它会无限期地阻塞。
这种“无限期阻塞”的行为肯定会导致错误1053。
如果您想要比这更完整的答案,可以下载源代码并自行调试。
但是,我愿意打赌,如果您正确处理线程管理问题(如上所述),您的问题将消失。
简而言之,将System.exit()调用留给Tomcat - 那不是您的工作。

我敢说99.99%的应用程序从不需要调用System.exit()。只需在主方法中退出循环或从线程的run()方法返回即可。 - Mr. Shiny and New 安宇
同意,Shiny先生。每当您看到System.exit()的调用时,都应该怀疑程序设计不良。 - Jared
1
我认为 keepGoing 应该被标记为 volatile(或者它的访问应该被同步),否则一些线程可能无法看到新值。 - Jerome
1
如果您想为程序提供退出状态,就像基本上所有命令行工具一样,那么需要使用System.exit(int)。 - Geoff
2
@Geoff - 是的。如果你正在编写一个需要操作退出代码的命令行实用程序,你需要调用System.exit()。如果你需要从与servlet相关的进程中操作退出代码,我会认真质疑你的系统设计。System.exit()只在你对整个系统有非常严格的控制的情况下才有用——servlet不是那种情况——某个人(容器)负责系统,而不是你的servlet。 - Jared

3
在Servlet的destroy()方法中调用System.exit()来强制杀死非守护线程是不明智的。这将强制关闭所有线程,其中可能包括Tomcat正在关闭的部分。这将导致Tomcat不正常关闭。这还可以防止运行关机处理程序,从而可能导致各种问题。 对于一个Servlet销毁后会执行大量代码,例如Context destroy和所有其他监听器等。通过调用System.exit,您将阻止所有这些代码的运行。更好的问题是什么是这些非守护线程,为什么它们在运行以及谁启动它们?

回答你的最后一个问题,有很多线程执行各种任务,如轮询、报告等。Servlet 初始化这些线程,然后它们开始运行。 - Nikhil Kashyap

0

当编写像Jared的线程关闭代码时,我通常会将“keepGoing”成员和“stopExecuting()”方法设置为静态,以便所有线程都可以通过一个关闭调用获得下降信号。这是个好主意吗?


我对所有静态事物都有一种根深蒂固的厌恶感,特别是在处理并发时。这并不意味着这种方法是错误的,或者它不能工作,或者它不够高效。我的抵触情绪可能有些过度了。 - Jared
实际上,在实践中,我通常会将MyThread实现为一个抽象类,其中doSomeStuff(),probablySleepForABit()和cleanup()都是抽象的...因为静态成员不会被继承,所以那样做行不通。 - Jared
编辑了我的答案,包括“servlet线程管理”的抽象。 - Jared
我会将keepGoing变量保持为非静态的,因为有时您只想关闭一个线程。将所有线程句柄放入一个集合中并迭代它以停止线程非常容易。 - Mr. Shiny and New 安宇

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接