Tomcat 6线程池用于异步处理

9

简短问题:在Tomcat 6应用程序中,如何运行(单独的)线程池?

运行线程池的最佳解决方案是什么?

长问题:
我有一个简单的需求;
轮询数据库以获取一些数据,同时允许Web客户端等待答案(长轮询连接)。
当这些数据在数据库中可用时,我将向相关客户端发送回复。

因此,我希望暂时不要深入任何框架(也许使用quartz scheduler?)。

因此,我得出结论,我需要一个线程池来在后台完成工作。

因此,如果我要使用Thread(实际上是Runnable),哪个类可以组织所有这些内容?是否有某种ThreadPool解决方案?有什么建议吗?


所以,你想要一个长时间运行的请求,等待后台任务轮询数据库?为什么不直接让客户端轮询数据库呢?这将大大简化你的软件。 - Christopher Schultz
@ChristopherSchultz,让Tomcat的HTTP请求处理程序工作线程执行长循环或类似操作是耗尽服务的可怕方式。想象一下,如果Tomcat有5个服务线程-如果我让这些线程被数据库轮询卡住,那么其他客户端就无法与系统通信。为此需要另一个线程池,即使这意味着使代码变得更加复杂(顺便说一下,这并不是太难)。 - Poni
3个回答

17

回答你的问题:

JVM中的线程池是通过 java.util.concurrent.ExecutorService 接口抽象的。尽管有不同的接口实现,但在大多数情况下,该接口的方法就足够使用了。

要创建特定的线程池,请查看 java.util.concurrent.Executors 类: http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/Executors.html 其中包含用于创建不同 ExecutorService 接口实现的静态工厂方法。您可能会对 newFixedThreadPool(int threadsNumber)newCachedThreadPool 方法感兴趣。

关于 JVM 中的 Executors 的更一般信息,您可以阅读 Oracle 的教程:http://docs.oracle.com/javase/tutorial/essential/concurrency/executors.html

因此,在 Tomcat 下使用线程池(ExecutorService)应按以下步骤执行:

1. 如果尚未完成,请在 web.xml 中创建并注册 javax.servlet.ServletContextListener 接口的实例(它将充当您的 Web 应用程序的入口点)。

2. 在 contextInitialized(ServletContextEvent) 方法中,创建 ExecutorService 实例(线程池),并将其存储在 ServletContext 属性映射中,以便可以从您的 Web 应用程序的任何点访问它,例如:

// following method is invoked one time, when you web application starts (is deployed)
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
    // ...
    final int numberOfThreads = ...;
    final ExecutorService threadPool = Executors.newFixedThreadPool(numberOfThreads); // starts thread pool
    final ServletContext servletContext = servletContextEvent.getServletContext();
    servletContext.setAttribute("threadPoolAlias", threadPool);
    // ...
}

// following method is invoked one time when your web application stops (is undeployed)
public void contextDestroyed(ServletContextEvent servletContextEvent) {
    // following code is just to free resources occupied by thread pool when web application is undeployed
    final ExecutorService threadPool = (ExecutorService) servletContextEvent.getServletContext().getAttribute("threadPoolAlias");
    threadPool.shutdown();
}

3. 在Servlet.service方法中的任何地方或者在你的webapp中的任何位置(你几乎可以从webapp中的任何地方获取对ServletContext的引用):

Callable<ResultOfMyTask> callable = new Callable<ResultOfMyTask>() {
    public ResultOfMyTask call() {
        // here goes your task code which is to be invoked by thread pool 
    }
};

final ServletContext servletContext = ...;
final ExecutorService threadPool = (ExecutorService) servletContext.getAttribute("threadPoolAlias");
final Future<ResultOfMyTask> myTask = threadPool.submit(callable);;

你应该存储对myTask的引用,并可以从其他线程查询它是否完成以及结果是什么。

希望这有所帮助...


这基本上就是我目前所拥有的 :) 我会让这个问题继续保持开放状态,以防有人提出另一个想法,但我已经感谢你抽出时间写下这样一个好答案。 - Poni
1
值得一提的是,contextInitializedcontextDestroyed方法应该位于实现ServletContextListener的自定义类中,并且在web.xml中告诉Tomcat关于该类的信息,例如<listener><listener-class>path.to.your.YourServletListener</listener-class></listener> - RTF
@RTF 是的,我在第1和第2项中提到了这一点,但可能不够清楚,所以感谢您的补充。 - Yuriy Nakonechnyy

0
对于简单的后台任务,您根本不需要任何类型的线程池。您需要做的是:
  1. 启动一个线程
  2. 检查后台进程是否应该停止
  3. 让它轮询数据库
  4. 将新轮询的数据存储在某个可访问的地方
  5. 检查后台进程是否应该停止
  6. 进入睡眠状态
  7. 重复步骤#2-#7
编写一个 ServletContextListener,它可以启动一个线程来执行上述步骤,并定义一个实现 Runnable 的类。在 contextDestroyed 方法中,设置一个标志触发上述 #2 和 #5 中指示的检查,然后调用 Thread.interrupt 以终止线程。
您的后台任务绝不能尝试同步向客户端发送消息。相反,使用其他机制通知等待轮询者,例如在监视器上使用 Object.notify(这实际上没有任何意义,因为您不想阻塞检查当前状态的客户端),更新某种时间戳,或者只是让轮询客户端检查#4中可用的当前数据。

我认为在Web服务器中生成线程可能不是一个好主意:你很容易就会拥有数百个线程,然后导致服务器崩溃。另一方面,线程池则没有这个问题,提供请求排队并避免了一直创建新线程的开销。 - Didier L
@DidierL 所以您不想在服务器上启动线程,但您的建议是使用线程池?除非您有一个(无用的)大小为0的线程池,否则您正在创建线程。我建议创建一个单独的线程并使用任务队列。除非有理由维护多个线程,否则线程池是不必要的开销。如果您从ServletContextListener管理线程,则资源管理不是问题。 - Christopher Schultz
嗯,我建议管理单个线程而不是使用线程池。 ;) 给一个线程添加一个 pleaseStop 方法并不难。 - Christopher Schultz
是的,抱歉我没有意识到您只想使用单个线程。但是在这种特殊情况下,用户似乎需要同时处理多个请求,因此可能需要更多线程,而线程池可能是最可靠(处理线程崩溃)和最简单的解决方案。 - Didier L

0

针对您的使用情况,您可以利用Java平台中提供的Timer和TimerTask来定期执行后台任务。

import java.util.Timer;
import java.util.TimerTask;

TimerTask dbTask = new TimerTask() {
    @Override
    public void run() {
        // Perform Db call and do some action
    }
};

final long INTERVAL = 1000 * 60 * 5; // five minute interval

// Run the task at fixed interval
new Timer(true).scheduleAtFixedRate(dbTask, 0, INTERVAL);

请注意,如果一个任务需要超过五分钟才能完成,那么后续的任务将不会并行执行。相反,它们将等待在队列中,并在前一个任务完成后快速连续执行。
您可以将此代码包装在单例类中,并从启动Servlet调用。
通常,最好在Servlet容器之外执行这些周期性后台作业,因为它们应该有效地用于服务HTTP请求。

1
问题要求使用线程池的解决方案。Timer 使用单个后台线程。而 ScheduledThreadPoolExecutor 则... - Sridhar

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