Contents

Java ExecutorService tutorial

Written by: David Vlijmincx

Introduction

Since Java 5, it is encouraged to use an executorService instead of using the Thread class directly. The executorService provides an easy-to-use API to submit tasks to a pool of threads.

Creating an instance of ExecutorService

Java provides multiple implementations of the ExecutorService interface. The Executors class has Factory and utility methods to create an instance of an ExecutorService. The following code snippet shows you how to create instances of the different ExecutorService implementations with a short description.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Uses a single thread to execute the submitted tasks.
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// Creates a thread when needed but will reuse older threads when available.
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// Uses a thread pool that reuses a fixed number of threads to execute the submitted tasks.
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);

// Creates a thread that executes a command after a delay or periodically.
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

// Same as newScheduledThreadPool, but only one task will run simultaneously.
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

// Uses a ForkJoinPool to execute the submitted tasks.
ExecutorService workStealingPool = Executors.newWorkStealingPool();

// Java 19
// Creates a thread for each task using the given factory to create threads.
ExecutorService threadPerTaskExecutor = Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory());

// Java 19, but you need to enable preview features
// Creates a virtual thread to execute each submitted task. 
ExecutorService virtualThreadPerTaskExecutor = Executors.newVirtualThreadPerTaskExecutor();

Using an ExecutorService

ExecutorService methods take a Runnable or Callable as a parameter. The threads inside the pool of the ExecutorService will run these tasks. In the following examples, I will show you how to submit tasks to an ExecutorService in multiple ways. The examples use a newFixedThreadPool, but you can use any executorService you need.

Execute a runnable

The execute method will run the task in a new thread. This is a fire-and-forget method, as we don't have anything to check or control the thread.

1
2
3
4
5
6
7
// Creating an executorService for the tasks
ExecutorService executorService = Executors.newFixedThreadPool(5);

// Execute a runnable using the execute method
executorService.execute(() -> {
    System.out.println(" Run a runnable");
});

Submitting a runnable

In the following example, we submit() a Runnable to the executorService. This will return an instance of Future. We can use this instance to check if the thread is done and to wait for the result using get().

1
2
3
4
5
6
7
8
9
// Submitting a runnable to run
ExecutorService executorService = Executors.newFixedThreadPool(5);

Future<?> submit = executorService.submit(() -> {
    System.out.println(" Run a runnable using submit");
});

// Wait for the result of the thread
submitCallable.get();

Submitting a callable

In the following example, we submit() a callable to the executorService. This will return an instance of Future. We can use this instance to check if the thread is done and to wait for the result using get(). In this case, the result is of type String.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ExecutorService executorService = Executors.newFixedThreadPool(5);

// creating a callable
Callable<String> callable = () -> {
    Thread.sleep(1000);
    return "Done";
};

// Submitting a callable to run
Future<String> future = executorService.submit(callable);

// retrieve value
try {
    future.get();
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
}

Using ExecutorService invokeAll

With invokeAll(), we can submit a number of tasks with one call. Using the list of Future instances, we can wait till all the threads are done. In the following example, you can see how to use the invokeAll method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
ExecutorService executorService = Executors.newFixedThreadPool(5);

// create your callables
Callable<String> callable1 = () -> {
    Thread.sleep(1000);
    return "Done";
};

// Store them inside a list
List<Callable<String>> listOfCallables = List.of(callable1, callable1, callable1);

// invoke all your callables using invokeAll
List<Future<String>> futures = executorService.invokeAll(listOfCallables);

// One way to retrieve the values from your list of Futures
List<String> stringList = futures.stream().map(f -> {
    try {
        return f.get();
    } catch (InterruptedException | ExecutionException e) {
        throw new RuntimeException(e);
    }
}).collect(Collectors.toList());

Using ExecutorService invokeAny

With invokeAny() we can submit a number of threads. The result of the first task that is done will be returned. In the following example, I submit three callables of type String. The invokeAny() returns the String value of the first callable to finish.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ExecutorService executorService = Executors.newFixedThreadPool(5);

// Create your callables
Callable<String> callable1 = () -> {
    Thread.sleep(1000);
    return "Done";
};

// Store them inside a list
List<Callable<String>> listOfCallables = List.of(callable1, callable1, callable1);

// Submit all your callables using invokeAll
String result = executorService.invokeAny(listOfCallables);

How to use the ScheduledExecutorService

With ScheduledExecutorService, you can schedule tasks to run in the future or to repeat a certain thread periodically.

Schedule a task to run once

With schedule() you schedule a task to run once in the future. It takes a callable, long, and time-unit as a parameter. With the second and third parameter, you can set the delay, after which the task will run. In the following example the task will start after 5 minutes.

1
2
3
4
5
6
7
8
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

Callable<String> scheduledCallable = () -> {
    Thread.sleep(1000);
    return "Done";
};

executorService.schedule(scheduledCallable, 5, TimeUnit.MINUTES);

Schedule a task to run at a fixed rate

With scheduleAtFixedRate, you start a task after every number of the time-unit that you choose. The first parameter is the Runnable that will run. The second parameter is the initial delay of the first time the task will run. The second Long is the time between starting the current task and the next one. The last parameter is the time unit you need.

In the following example, we run the first task after waiting one minute. After the initial delay, we will start a new task every two minutes.

1
2
3
4
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

Runnable runnable = () -> System.out.println("Running");
executorService.scheduleAtFixedRate(runnable, 1, 5, TimeUnit.MINUTES);

Schedule a task to run at a delay

With scheduleWithFixedDelay we can set a task to repeat multiple times. The first parameter is the runnable we want to run. The second parameter is the initial delay. The third parameter is the time between finishing the previous task and starting the next one. The last parameter is the time unit you need.

In the following example, we start a thread after waiting one minute. We then wait till the previous task is done plus two minutes before starting the next task.

1
2
3
4
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

unnable runnable = () -> System.out.println("Running");
executorService.scheduleWithFixedDelay(runnable, 1, 5, TimeUnit.MINUTES);

Shutdown an ExecutorService

Shutting down an ExecutorService is kind of tricky. The best way to do so can be found inside the Java documentation. In the following code snippet, you can also find the code from the documentation to shut down an ExecutorService.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Code from the Java documentation to shut down an executorService
void shutdownAndAwaitTermination(ExecutorService pool) {
   pool.shutdown(); // Disable new tasks from being submitted
   try {
     // Wait a while for existing tasks to terminate
     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
       pool.shutdownNow(); // Cancel currently executing tasks
       // Wait a while for tasks to respond to being cancelled
       if (!pool.awaitTermination(60, TimeUnit.SECONDS))
           System.err.println("Pool did not terminate");
     }
   } catch (InterruptedException ie) {
     // (Re-)Cancel if current thread also interrupted
     pool.shutdownNow();
     // Preserve interrupt status
     Thread.currentThread().interrupt();
   }
 }

Shutdown ExecutorService in Java 19

With Java 19, the ExecutorService implements the autocloseable interface. This makes it a lot easier to shut down an ExecutorService. In the following example, you can see how we do this. Before you would exit the try Java will close the ExecutorService for you. This is a great improvement. Do keep in mind that in the worst case it will await termination for 1 day.

1
2
3
4
try (ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor()) {
    
    singleThreadExecutor.submit(() -> "running inside a try");
}

Conclusion

In this article, we saw how to use the ExecutorService and submit tasks to the underlying thread pool.

Further reading