The newFixedThreadPool method of the Executors class creates an executorService with a fixed number of threads. Unlike the newSingleThreadExecutor method, we specify how many threads we want in the pool. Under the hood, the following code is called:


new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

The corePoolSize (the number of threads that will be ready (started) when the executor service starts) and maximumPoolSize (the maximum number of threads that the executor service can create) parameters receive the same value — the number of threads passed to newFixedThreadPool(nThreads). And we can pass our own implementation of ThreadFactory in exactly the same way.

Well, let's see why we need such an ExecutorService.

Here's the logic of an ExecutorService with a fixed number (n) of threads:

  • A maximum of n threads will be active for processing tasks.
  • If more than n tasks are submitted, they will be held in the queue until threads become free.
  • If one of the threads fails and terminates, a new thread will be created to take its place.
  • Any thread in the pool is active until the pool is shut down.

As an example, imagine waiting to go through security at the airport. Everyone stands in one line until immediately before the security check, passengers are distributed among all the working checkpoints. If there is a delay at one of the checkpoints, the queue will be processed by only the second one until the first one is free. And if one checkpoint closes entirely, then another checkpoint will be opened to replace it, and passengers will continue to be processed through two checkpoints.

We'll note right away that even if conditions are ideal — the promised n threads work stably, and threads that end with an error are always replaced (something that limited resources make impossible to achieve in a real airport) — the system still has several unpleasant features, because under no circumstances will there be more threads, even if the queue grows faster than the threads can process tasks.

I suggest getting a practical understanding of how ExecutorService works with a fixed number of threads. Let's create a class that implements Runnable. Objects of this class represent our tasks for the ExecutorService.


public class Task implements Runnable {
    int taskNumber;
 
    public Task(int taskNumber) {
        this.taskNumber = taskNumber;
    }
 
    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}
    

In the run() method, we block the thread for 2 seconds, simulating some workload, and then display the current task's number and the name of the thread executing the task.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }
        
        executorService.shutdown();
    

To begin with, in the main method, we create an ExecutorService and submit 30 tasks for execution.

Processed user request #1 on pool-1-thread-2 thread
Processed user request #0 on pool-1-thread-1 thread
Processed user request #2 on pool-1-thread-3 thread
Processed user request #5 on pool-1-thread-3 thread
Processed user request #3 on pool-1-thread-2 thread
Processed user request #4 on pool-1-thread-1 thread
Processed user request #8 on pool-1-thread-1 thread
Processed user request #6 on pool-1-thread-3 thread
Processed user request #7 on pool-1-thread-2 thread
Processed user request #10 on pool-1-thread-3 thread
Processed user request #9 on pool-1-thread-1 thread
Processed user request #11 on pool-1-thread-2 thread
Processed user request #12 on pool-1-thread-3 thread
Processed user request #14 on pool-1-thread-2 thread
Processed user request #13 on pool-1-thread-1 thread
Processed user request #15 on pool-1-thread-3 thread
Processed user request #16 on pool-1-thread-2 thread
Processed user request #17 on pool-1-thread-1 thread
Processed user request #18 on pool-1-thread-3 thread
Processed user request #19 on pool-1-thread-2 thread
Processed user request #20 on pool-1-thread-1 thread
Processed user request #21 on pool-1-thread-3 thread
Processed user request #22 on pool-1-thread-2 thread
Processed user request #23 on pool-1-thread-1 thread
Processed user request #25 on pool-1-thread-2 thread
Processed user request #24 on pool-1-thread-3 thread
Processed user request #26 on pool-1-thread-1 thread
Processed user request #27 on pool-1-thread-2 thread
Processed user request #28 on pool-1-thread-3 thread
Processed user request #29 on pool-1-thread-1 thread

The console output shows us how the tasks are executed on different threads once they are released by the previous task.

Now we'll increase the number of tasks to 100, and after submitting 100 tasks, we'll call the awaitTermination(11, SECONDS) method. We pass a number and time unit as arguments. This method will block the main thread for 11 seconds. Then we will call shutdownNow() to force the ExecutorService to shut down without waiting for all tasks to complete.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }
 
        executorService.awaitTermination(11, SECONDS);
 
        executorService.shutdownNow();
        System.out.println(executorService);
    

At the end, we'll display information about the state of the executorService.

Here's the console output we get:

Processed user request #0 on pool-1-thread-1 thread
Processed user request #2 on pool-1-thread-3 thread
Processed user request #1 on pool-1-thread-2 thread
Processed user request #4 on pool-1-thread-3 thread
Processed user request #5 on pool-1-thread-2 thread
Processed user request #3 on pool-1-thread-1 thread
Processed user request #6 on pool-1-thread-3 thread
Processed user request #7 on pool-1-thread-2 thread
Processed user request #8 on pool-1-thread-1 thread
Processed user request #9 on pool-1-thread-3 thread
Processed user request #11 on pool-1-thread-1 thread
Processed user request #10 on pool-1-thread-2 thread
Processed user request #13 on pool-1-thread-1 thread
Processed user request #14 on pool-1-thread-2 thread
Processed user request #12 on pool-1-thread-3 thread
java.util.concurrent.ThreadPoolExecutor@452b3a41[Shutting down, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 15]
Processed user request #17 on pool-1-thread-3 thread
Processed user request #15 on pool-1-thread-1 thread
Processed user request #16 on pool-1-thread-2 thread

This is followed by 3 InterruptedExceptions, thrown by sleep methods from 3 active tasks.

We can see that when the program ends, 15 tasks are done, but the pool still had 3 active threads that did not finish executing their tasks. The interrupt() method is called on these three threads, which means that the task will complete, but in our case, the sleep method throws an InterruptedException. We also see that after the shutdownNow() method is called, the task queue is cleared.

So when using an ExecutorService with a fixed number of threads in the pool, be sure to remember how it works. This type is suitable for tasks with a known constant load.

Here's another interesting question: if you need to use an executor for a single thread, which method should you call? newSingleThreadExecutor() or newFixedThreadPool(1)?

Both executors will have equivalent behavior. The only difference is that the newSingleThreadExecutor() method will return an executor that cannot be later reconfigured to use additional threads.