javamultithreadingthreadpoolthreadpoolexecutorscheduledexecutorservice

Using separate thread pools for normal and scheduled tasks


Suppose I already have an application-wide ThreadPoolExecutor, and now I also need a ScheduledExecutorService. Should I just change the implementation to ScheduledThreadPoolExecutor and use that for both, scheduled and ordinary tasks? Or should I have a ThreadPoolExecutor for ordinary tasks, and a separate ScheduledThreadPoolExecutor for scheduled tasks? Is it a good idea to use ScheduledThreadPoolExecutor for ordinary tasks, or is there something in it's implementation that makes it only useful for scheduled tasks?

My line of thinking is that a single ScheduledThreadPoolExecutor would allow for the best thread usage, while it increases the risk of starvation if we have many scheduled tasks and few ordinary ones or vice versa.

For more context, I have an old codebase that still uses java.util.Timer for scheduled tasks, and an application-wide ThreadPoolExecutor for ordinary tasks. Since Timer is single-threaded, our TimerTasks typically shift computation immediately to the thread pool once the Timer executes them, in order not to delay further TimerTasks. Something along these lines:

timer.schedule(new TimerTask() {
  public void run() {
    executor.execute(this::performWork);
  }
}, 1000);

So far we postponed refactoring from Timer to ScheduledThreadPoolExecutor, but it's only now that I realize that we can eliminate constructs like the one above when using a single thread pool for all kinds of tasks.


Solution

  • I would recommend using two separate pools for two reasons. The first reason is that I find it easier from an application perspective to tune the pools if they are distinct. It is probably easier to predict the number of threads needed for scheduled tasks and for one-shot tasks than the combined workload. The second reason is from the API:

    While this class inherits from ThreadPoolExecutor, a few of the inherited tuning methods are not useful for it. In particular, because it acts as a fixed-sized pool using corePoolSize threads and an unbounded queue, adjustments to maximumPoolSize have no useful effect. Additionally, it is almost never a good idea to set corePoolSize to zero or use allowCoreThreadTimeOut because this may leave the pool without threads to handle tasks once they become eligible to run.

    In other words it doesn't quite behave as a regular ThreadPoolExecutor and tuning efforts can have surprising effects including starvation.