1. Overview

In this tutorial, we're going to examine how we can create a thread pool using ExecutorService. We'll start with the Executors class since it's the most convenient approach. Then we'll manually create thread pools using ThreadPoolExecutor and also examine its configuration properties.

2. Create using Executors

The primary way of creating a thread pool is using the Executors class. It contains several factory methods to initialize thread pools with different runtime characteristics.

2.1. newFixedThreadPool

newFixedThreadPool creates a thread pool with the given number of worker threads and an unbounded queue. The underlying queue is FIFO ordered, so the order of submission matters.

public void fixedSizePool() {
    final ExecutorService executorService = Executors.newFixedThreadPool(5);
    executorService.shutdown();
}

Here, we're creating a thread pool with five threads.

Initially, the pool starts with zero threads. It then creates a new thread whenever a new task is submitted - until it reaches the given thread count:

public void fixedSizePoolThreadCreation() {
    final ExecutorService executorService = Executors.newFixedThreadPool(5);
    printPoolSize(executorService);

    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
            TimeUnit.SECONDS.sleep(1);
            return "done";
        });

        printPoolSize(executorService);
    }

    executorService.shutdown();
}
 
private void printPoolSize(ExecutorService executorService) {
    if (executorService instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executorService;
        System.out.println("Current pool size: " + threadPool.getPoolSize());
        System.out.println("Core pool size: " + threadPool.getCorePoolSize());
    }
}

In this example, we're again creating a pool of five threads. Then we're submitting more tasks than the pool size, ten in our case. To inspect the pool size, we're declaring the printPoolSize() method. It checks whether the executor is an instance of ThreadPoolExecutor which is a common implementation class used by newFixedThreadPool and other factory methods. We're then printing the pool size, core pool size, and maximum pool size. In the next sections, we'll talk more about these properties.

When we run the application, we can see the thread creation pattern:

Current pool size: 0  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 1  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 2  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 3  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 4  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 5  <-
Core pool size: 5
Maximum pool size: 5
Current pool size: 5  <-
Core pool size: 5
Maximum pool size: 5
...

As expected, the pool starts with zero threads, although the core pool size is five. Then it creates new threads as the tasks arrive. After the pool reaches its thread limit, the pool size remains the same. And the existing threads don't timeout and get terminated due to inactivity.

2.2. newCachedThreadPool

Unlike newFixedThreadPool, newCachedThreadPool doesn't impose any constraints on the thread count, it's unbounded. When a new task arrives and if there are no available threads, the pool creates a new one. This also means that tasks don't wait in the queue and run immediately - given that the system has the required resources.

public void cachedPool() {
    final ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.shutdown();
}

Similar to newFixedThreadPool, the pool starts with zero threads:

public void cachedPoolThreadCreation() {
    final ExecutorService executorService = Executors.newCachedThreadPool();
    printPoolSize(executorService);

    for (int i = 0; i < 100; i++) {
        executorService.submit(() -> {
            TimeUnit.SECONDS.sleep(1);
            return "done";
        });

        printPoolSize(executorService);
    }

    executorService.shutdown();
}

In a sample run, it prints:

Current pool size: 0
Core pool size: 0
Maximum pool size: 2147483647
Current pool size: 1
Core pool size: 0
Maximum pool size: 2147483647
Current pool size: 2
...
Current pool size: 99
Core pool size: 0
Maximum pool size: 2147483647
Current pool size: 100
Core pool size: 0
Maximum pool size: 2147483647

Here, the current pool size increases without a practical bound. However, if a worker thread sits idle for 60 seconds, the pool terminates it. 

2.3. newSingleThreadExecutor

newSingleThreadExecutor creates a thread pool with a single worker thread and an unbounded queue. This means that submitted tasks will run sequentially in FIFO order:

public void singleThreadedPool() {
    final ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.shutdown();
}

2.4.newScheduledThreadPool

Lastly, we'll create a thread pool that can schedule tasks to run with a delay or periodicallynewScheduledThreadPool expects a thread count to size the pool:

public void scheduledPool() {
    final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    scheduledExecutorService.shutdown();
}

3. Create Manually using ThreadPoolExecutor

So far, we've used Executors to create our thread pools. We also hinted that it uses the ThreadPoolExecutor class which is an implementation of ExecutorService. In the following examples, we'll directly use ThreadPoolExecutor to customize the different aspects of the pool.

3.1. Using ThreadPoolExecutor

ThreadPoolExecutor is an important pool implementation that is used by newSingleThreadExecutor, newFixedThreadPool and newCachedThreadPool.

We'll now directly instantiate it:

public void configureThreadPool() {
    final int corePoolSize = 10;
    final int maximumPoolSize = 10;
    final int keepAliveTime = 0;
    final BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
    final ThreadFactory threadFactory = Executors.defaultThreadFactory();
    final RejectedExecutionHandler handler = new AbortPolicy();
    
    final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize,
      maximumPoolSize,
      keepAliveTime, TimeUnit.SECONDS,
      taskQueue,
      threadFactory,
      handler);
}

As shown in the example, a sample instantiation expects some configuration values:

  • Core pool size: This is the number of threads that the pool won't terminate even if they're idle. This is true unless allowCoreThreadTimeOut is set.
  • Maximum pool size: This is the maximum number of threads. So the pool first tries to handle incoming tasks by the core worker threads, then buffers them in the queue. But if the queue gets full, the pool increases its thread count up to this maximum value.
  • Keep alive time: If there are more threads than the core pool size, the pool terminates the ones that sit idle for this amount of time.
  • Task queue: Task queue must be blocking - a BlockingQueue implementation. We have three choices here: bounded queue, unbounded queue, or synchronous queue.
  • Thread factory: Whenever an ExecutorService creates a new thread, it does so using a ThreadFactory instance.
  • Saturation policy: Every thread pool needs a RejectedExecutionHandler to manage its saturation policy. This handler manages what to do in case the submitted task can't be run or stored.

We can also modify these properties after the thread pool is initialized:

threadPool.setMaximumPoolSize(12);
threadPool.setCorePoolSize(11);
threadPool.setKeepAliveTime(1, TimeUnit.SECONDS);
threadPool.setRejectedExecutionHandler(new CallerRunsPolicy());
threadPool.setThreadFactory(new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r);
    }
});

In this example, we're modifying the pool using ThreadPoolExecutor's setter methods.

Remember that when we use the Executors factory methods, we don't provide most of these properties. But we can modify the returned pools in the same manner - except for newSingleThreadExecutor which returns an unconfigurable instance:

public void configureFactoryReturnedThreadPool() {
    final ExecutorService executorService = Executors.newFixedThreadPool(10);
    if (executorService instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executorService;
        threadPool.setMaximumPoolSize(12);
        threadPool.setCorePoolSize(11);
        threadPool.setKeepAliveTime(1, TimeUnit.SECONDS);
        threadPool.setRejectedExecutionHandler(new CallerRunsPolicy());
        threadPool.setThreadFactory(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        });
    }
}

3.2. Using ScheduledThreadPoolExecutor

Next, we'll create a thread pool using the ScheduledThreadPoolExecutor class instead of Executors.newScheduledThreadPool. Since ScheduledThreadPoolExecutor extends ThreadPoolExecutor, it provides similar configuration properties:

public void configureScheduledThreadPool() {
    final int corePoolSize = 10;
    final ThreadFactory threadFactory = Executors.defaultThreadFactory();
    final RejectedExecutionHandler handler = new AbortPolicy();
    final ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory, handler);

    threadPool.setMaximumPoolSize(100);
    threadPool.setCorePoolSize(20);
    threadPool.setKeepAliveTime(1, TimeUnit.SECONDS);
    threadPool.setRejectedExecutionHandler(new CallerRunsPolicy());
    threadPool.setThreadFactory(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    threadPool.shutdown();
}

In this example, we're first creating an instance of ScheduledThreadPoolExecutor and then modifying its properties through setter methods.

4. Close to Configuration

So far we have seen two approaches to create a thread pool and both allowed us to configure the pool after the instantiation - with the exception of Executors.newSingleThreadExecutor. Next, we'll make our ExecutorService instances unconfigurable using Executors.unconfigurableExecutorService:

public void unconfigurableThreadPool() {
    final ExecutorService threadPool = Executors.newFixedThreadPool(5);
    final ExecutorService unconfigurableThreadPool = Executors.unconfigurableExecutorService(threadPool);

    unconfigurableThreadPool.shutdown();
}

Here, we're first creating an ExecutorService and then passing it to Executors.unconfigurableExecutorService.

5. Summary

In this tutorial, we've examined the different ways to create a thread pool using Java ExecutorService. We first used the Executors class which contains some convenient factory methods. Then we directly instantiated the pools using ThreadPoolExecutor and ScheduledThreadPoolExecutor. Throughout the examples, we also studied the available configuration options.

Lastly, check out the source code for all examples in this tutorial over on Github.