在Tomcat中从servlet生成线程的推荐方法是什么?

51

可能是重复问题!我正在使用Tomcat作为我的服务器,想知道在Servlet中生成线程以实现确定性结果的最佳方法。我从一个Servlet操作中运行一些长时间运行的更新,并希望请求完成并且更新在后台中发生。除了添加像RabbitMQ这样的消息中间件之外,我认为我可以生成一个线程,在后台运行并按照自己的时间完成。我在其他SO线程中读到,服务器终止由服务器生成的线程,以便管理资源。

在使用Tomcat时,是否有推荐的生成线程或后台任务的方法?我还使用Spring MVC进行应用程序开发。


5个回答

47
在像Tomcat或Jetty这样的简易Servlet容器中,最安全的选择是使用一个应用程序范围内的具有最大线程数的线程池,以便在必要时任务将被排队。这时ExecutorService非常有帮助。
在应用程序启动或servlet初始化时,请使用Executors类创建一个线程池:
executor = Executors.newFixedThreadPool(10); // Max 10 threads.

然后在Servlet的service方法中(如果您不感兴趣,可以忽略结果,或者将其存储在会话中以供稍后访问):

Future<ReturnType> result = executor.submit(new YourTask(yourData));

YourTask必须实现RunnableCallable接口,可以像这样编写代码,其中yourData是你的数据,例如填充请求参数值(请注意,绝对不应传递Servlet API工件,例如HttpServletRequestHttpServletResponse):

public class YourTask implements Runnable {

    private YourData yourData;

    public YourTask(YourData yourData) {
        this.yourData = yourData;
    }

    @Override
    public void run() {
        // Do your task here based on your data.
    }
}

最后,在应用程序关闭或servlet销毁期间,您需要显式地关闭它,否则线程可能会永远运行并阻止服务器正确关闭。

executor.shutdownNow(); // Returns list of undone tasks, for the case that.

如果您实际上正在使用像WildFly、Payara、TomEE等普通的JEE服务器,在那里EJB通常是可用的,那么您可以简单地在从servlet调用的EJB方法上放置@Asynchronous注释。您还可以选择让它返回一个Future<T>,其中AsyncResult<T>作为具体值。

@Asynchronous
public Future<ReturnType> submit() {
    // ... Do your job here.

    return new AsyncResult<ReturnType>(result);
}

参考文献:


13
如果你不将那些执行器线程标记为守护线程,可能会阻止Tomcat关闭,当其中一个任务失控时。将ThreadFactory传递给Executors.newXXX方法可以实现标记为守护线程。同时,使用ServletContextListener关闭创建的执行器是必须的。 - nos
2
@nos:关于守护线程,你说得很好。但有时为了避免破损的结果(例如可能会写入文件或数据库等),您希望它们完成任务。这完全取决于功能需求,这在原始问题中尚不清楚。另外,如果这些线程将被单个servlet使用,请在servlet的init()destroy()中创建和关闭它们即可。否则,在ServletContextListener中执行此操作确实更好,这样您就可以将其提供给更多的servlet。 - BalusC
我喜欢这个解决方案,但是我不会失去工作的上下文吗?我希望根据生成线程的人构建一些逻辑,并对其执行一些操作。Quartz解决方案看起来也非常不错。 - Ritesh M Nayak
然后将其作为 CallableTask 的构造函数参数传递即可。 - BalusC
2
我该如何确定Executors.newFixedThreadPool(n)中所需的最大线程数?我正在运行一个多用户应用程序,我预计大多数用户会生成1-10个线程(可能更多)。但我不知道最终会有多少用户。 - Timo Ernst

30
您可以使用一个像Foo-CommonJ这样的 CommonJ WorkManager (JSR 237) 实现:
CommonJ - JSR 237计时器和WorkManager是一个实现。Foo-CommonJ是JSR 237计时器和WorkManager的实现。它被设计用于那些不带自己实现的容器,主要是像Tomcat这样的纯Servlet容器。它也可以在完全成熟的Java EE应用程序服务器中使用,这些服务器没有WorkManager API或具有非标准API,如JBoss。
为什么要使用WorkManagers?通常的用例是,Servlet或JSP需要从多个来源聚合数据并在一个页面中显示它们。在类似于J2EE容器的托管环境中进行自己的线程处理是不合适的,绝不能在应用程序级别的代码中完成。在这种情况下,可以使用WorkManager API并行检索数据。
安装/部署CommonJ:JNDI资源的部署取决于供应商。该实现附带一个工厂类,实现javax.naming.spi.ObjectFactory接口,使其易于在最流行的容器中部署。它也可作为JBoss服务提供。更多...
更新:为了澄清,这是Java EE并发实用程序预览版(似乎是JSR-236和JSR-237的后继者)关于非托管线程的写法:
2.1 容器管理与非容器管理线程
Java EE 应用服务器要求资源管理以便于集中管理和保护应用组件免受消耗不必要资源的影响。这可以通过资源池化和管理资源的生命周期来实现。在服务器应用组件(如 Servlet 或 EJB)中使用 Java SE 并发工具,如 java.util.concurrency API、java.lang.Thread 和 java.util.Timer 是有问题的,因为容器和服务器对这些资源没有任何了解。
通过扩展 java.util.concurrent API,应用服务器和 Java EE 容器可以意识到所使用的资源,并为异步操作提供适当的执行上下文。
这主要是通过提供主要 java.util.concurrent.ExecutorService 接口的管理版本来实现的。
所以,在我看来没有什么新东西,旧问题仍然存在,未经管理的线程仍然是未经管理的线程。
  • 它们对应用服务器是未知的,没有访问 Java EE 上下文信息的权限。
  • 它们可以使用应用服务器后面的资源,但由于缺乏控制其数量和资源使用的管理能力,这可能会影响应用服务器从故障中恢复资源或优雅关闭的能力。

参考文献


1
需要注意的是,这个JSR是在2003年发布的,早于Java 1.5中引入的java.util.concurrent,后者使线程管理更加健壮。 - BalusC
@BalusC 是的,JSR在2003年就已经启动了。但是据我所知,JSR-236和JSR-237仍在进行中(即使它们以另一个名称存在)。请参阅Doug Lea维护的Java EE Concurrency Interest Site以获取最新版本。虽然我不确定确切的状态,但问题并不是真正的鲁棒性,而是容器本质上不知道未管理的线程。 - Pascal Thivent
是的,了解这些线程的容器将在线程管理方面带来更多好处。这样可以在容器级别而不是Web应用程序级别进行控制。感谢更新,信息很有趣。 - BalusC
@BalusC 是的,完全正确。我不知道为什么JSR-236和JSR-237没有被纳入Java EE 6。根据JSR-316的主页,它已经被考虑过了。而且有这样的需求...当然,并不是说推迟Java EE 6是一件好事,只是想知道为什么。 - Pascal Thivent

7
我知道这是一个老问题,但人们一直在问它,一直试图做这种事情(在处理servlet请求时显式生成线程)...... 从多个方面来看,这是一个非常有缺陷的方法......仅仅声明Java EE容器不赞成这种做法是不够的,尽管通常是正确的。
最重要的是,您永远无法预测servlet在任何给定时间会接收到多少并发请求。按照定义,Web应用程序、Servlet应该能够同时处理给定端点上的多个请求。如果你编程你的请求处理逻辑来明确启动一定数量的并发线程,那么你就有可能面临一个几乎不可避免的情况:线程用尽,应用程序停顿。您的任务执行者始终配置为使用有限合理大小的线程池。通常情况下,它不会超过10-20个(您不希望有太多线程来执行您的逻辑 - 根据任务的性质、它们竞争的资源、服务器上的处理器数量等因素而定)。假设您的请求处理程序(例如MVC控制器方法)调用一个或多个@Async注释的方法(在这种情况下,Spring抽象了任务执行器并使事情变得容易),或者明确使用任务执行器。当您的代码执行时,它开始从池中获取可用的线程。如果您一次只处理一个请求并且没有立即随后的请求,则可以。 (在这种情况下,您可能试图使用错误的技术来解决问题。)但是,如果它是向任意(或甚至已知)客户端公开的Web应用程序,他们可能会用请求攻击该端点,那么您将很快耗尽线程池,并且请求将开始堆积,等待线程可用。仅出于这个原因,您应该意识到如果您在考虑这样的设计,您可能走错了路。
更好的解决方案可能是异步地分段要处理的数据(可以是队列或任何其他类型的临时/分段数据存储)并返回响应。有一个外部、独立的应用程序,甚至可以有多个实例(部署在您的Web容器之外),轮询分段端点并在后台处理数据,可能使用有限数量的并发线程。这种解决方案不仅会给您异步/并发处理的优势,而且还能扩展,因为您将能够运行尽可能多的这种轮询器实例,它们可以被分发,指向分段端点。 HTH

6

Spring支持通过spring-scheduling实现异步任务(在您的情况下是长时间运行)。我建议不直接使用Java线程,而是与Quartz一起使用。

资源:


+1 Spring + quartz 很容易设置,并且在许多情况下都能很好地完成工作。 - bwawok
我发现石英调度程序机制非常好。我正在使用TaskScheduler bean来执行所有的后台处理。 - Ritesh M Nayak

4

严格来说,根据Java EE规范,您不允许创建线程。如果多个请求同时到达,我还会考虑拒绝服务攻击(故意或无意)。

使用中间件解决方案肯定更加健壮和符合标准。


JEE规范的问题在于,在JEE5中您无法使用异步调用,因为@Asynchronous仅在JEE6中添加。我同意大多数情况下使用线程是一个坏主意,但有时候你必须这样做。 - Colin Hebert
2
@Colin Java EE 5拥有JMS或WorkManager API,@Asynchronous绝对不是所有后台作业的万能药。 - Pascal Thivent
中间件方案是我最先考虑的,问题在于这需要大量重新架构和时间成本较高。而且这种操作的数量也不是太多。 - Ritesh M Nayak
2
这个问题是关于Servlets而不是JEE的。 - Raedwald

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