1. Overview

The Executors class provides several factory methods for creating instances of ExecutorService, Callable, and others. Although the most used methods are the ones that create ExecutorService instances, others also provide convenient shortcuts. In this quick tutorial, we're going to look at the Executors class and investigate some of its important methods.

2. Methods Returning ExecutorService

We'll start with the methods that return an ExecutorService implementation.

  • newSingleThreadExecutor(): It creates a thread pool consisting of a single thread and an unbounded queue. Thus it doesn't support concurrent execution and runs the tasks sequentially.
public void singleThread() {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
}
  • newFixedThreadPool(int nThreads): It creates a thread pool that maintains a fixed number of threads using a shared unbounded queue. The number of active threads can't exceed the upper limit nThreads.
public void fixedSize() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
}
  • newCachedThreadPool(): It creates a thread pool that creates new threads as new tasks arrive. So there is no limit on the thread count as long as the system resources allow. Additionally, it uses a SynchronousQueue to directly hand off the tasks to a worker thread. It also means that tasks don't wait in the task queue for execution.
public void cached() {
    ExecutorService executorService = Executors.newCachedThreadPool();
}

<h2id="methods-returning-scheduledexecutorservice">3. Methods Returning ScheduledExecutorService

Now we'll look at the methods that return a ScheduledExecutorService instance.

  • newSingleThreadScheduledExecutor(): It returns a thread pool that can schedule tasks. Moreover, since the pool contains only one thread, the tasks execute sequentially.
public void singleThreadScheduled() {
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
}
  • newScheduledThreadPool(int corePoolSize): It creates a thread pool that maintains a fixed number of threads, corePoolSize. Similar to the previous example, it can schedule tasks to run after a given delay or to execute periodically.
public void scheduled() {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
}

4. Methods Disabling Thread Pool Reconfiguration

4.1. Default Behavior

The Executors class allows us to define some configuration properties through its factory methods. For example, we can pass the pool size to the newFixedThreadPool factory method. However, we know that newFixedThreadPool returns a ThreadPoolExecutor instance and it allows further configuration options like maximum pool size, saturation policy, and others. Luckily, we can change these configuration properties after the thread pool construction by applying a cast:

public void configureAfterConstruction() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    if (executorService instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executorService;
        threadPool.setMaximumPoolSize(10);
    }
}

Here, we're acquiring an ExecutorService instance by invoking newFixedThreadPool. Then we're casting it to ThreadPoolExecutor. This allows us to change its maximum pool size.

To summarize, newFixedThreadPool, newCachedThreadPool and newScheduledThreadPool returns instances that we can reconfigure. However, newSingleThreadExecutor and newSingleThreadScheduledExecutor returns unconfigurable ExecutorService instances.

4.2. unconfigurableExecutorService

On the contrary, if we want the thread pool properties to remain unchanged, we can use the unconfigurableExecutorService method. It creates a wrapper object around the original ExecutorService and prevents reconfiguration:

public void preventReconfiguration() {
    ExecutorService initialThreadPool = Executors.newFixedThreadPool(5);
    final ExecutorService unconfigurableThreadPool = Executors.unconfigurableExecutorService(initialThreadPool);
    if (unconfigurableThreadPool instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) initialThreadPool;
        threadPool.setMaximumPoolSize(10);
    }
}

Here, we've acquired the thread pool by invoking newFixedThreadPool. Alternatively, we can create it manually:

public void preventReconfigurationAgain() {
    ExecutorService initialThreadPool = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    final ExecutorService unconfigurableThreadPool = Executors.unconfigurableExecutorService(initialThreadPool);
    if (unconfigurableThreadPool instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) initialThreadPool;
        threadPool.setMaximumPoolSize(10);
    }
}

In both examples, we wrap the original ExecutorService instance and close it to modification.

4.3. unconfigurableScheduledExecutorService

Similar to ExecutorService, we'll now prevent reconfiguration of the ScheduledExecutorService instances. For this purpose, Executors provides the unconfigurableScheduledExecutorService method:

public void preventReconfigurationForScheduledExecutorService() {
    ScheduledExecutorService initialThreadPool = Executors.newScheduledThreadPool(5);
    final ExecutorService unconfigurableThreadPool = Executors.unconfigurableExecutorService(initialThreadPool);
    if (unconfigurableThreadPool instanceof ThreadPoolExecutor) {
        final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) initialThreadPool;
        threadPool.setMaximumPoolSize(10);
    }
}

5. Methods Creating Callable

The Executors class allows us to use different types as Callable. We'll focus on adapting a Runnable to a Callable:

public void adaptRunnableToCallable() throws Exception {
    final Runnable runnableTask = new Runnable() {
        @Override
        public void run() {
            System.out.println("Doing work...");
        }
    };

    final Callable<Object> callable = Executors.callable(runnableTask);
}

When we submit this Callable task to a thread pool:

final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Future<Object> future = executorService.submit(callable);
final Object result = future.get(); // <-- result is null

The corresponding Future handle returns null since the original Runnable task doesn't define a return value.

Now we'll provide the return value when converting a Runnable into a Callable:

public void adaptRunnableToCallableWithReturnValue() throws Exception {
    final Runnable runnableTask = new Runnable() {
        @Override
        public void run() {
            System.out.println("Doing work...");
        }
    };

    final Callable<Object> callable = Executors.callable(runnableTask, "Done");
}

Now the resulting Callable task - and the Future handle - will return "Done" when completed.

6. Methods Returning ThreadFactory

Every ExecutorService implementation uses a ThreadFactory to create new worker threads. And the ExecutorService instances returned from the Executors factory methods use a DefaultThreadFactory instance. We'll now use the defaultThreadFactory method to create one:

public void defaultThreadFactory() {
    final ThreadFactory threadFactory = Executors.defaultThreadFactory();
}

7. Summary

In this quick tutorial, we've investigated some commonly used methods of the Executors class. We firstly examined the ones that return thread pool instances. We learned that each method provides a specific pool configuration tailored for a specific use case. Then we examined the methods that disable thread pool configuration, provide ThreadFactory instances, or adapt other types to Callable.

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