How to Run Tasks in Parallel with ExecutorService and CompletableFuture in Java


Let’s explore how we can take advantage of asynchronous operations from ExecutorService and CompletableFuture to run tasks in parallel.

1. Create ExecutorService

Let’s first create a new thread pool that reuses some fixed number of threads.

ExecutorService executorService = Executors.newFixedThreadPool(10);

Of course, depending on our use case, we can use other thread pool implementations:

2. Generate CompletableFuture list (async tasks)

Next, we’ll create a list of CompletableFuture objects and append some tasks that we want to run asynchronously.

List<CompletableFuture> futures = new ArrayList();
for (/* Iterate through stuff to do */) {
  futures.add(doStuffAsync());
}

Here’s an example of a task that returns a CompletableFuture.

CompletableFuture<Void> doStuffAsync() {
  return CompletableFuture.runAsync(() -> doStuff(), executorService)
}

Some common methods to generate a CompletableFuture include:

Here, we’re using runAsync() to execute some action (i.e. doStuff()) asynchronously with no inputs or outputs.

3. Execute async tasks with CompletableFuture::allOf

We can use CompletableFuture::allOf to run all of the tasks in parallel. It returns a new CompletableFuture that completes when all of the CompletableFuture tasks are complete.

CompletableFuture
  .allOf(futures.toArray(CompletableFuture[]::new))
  .thenRun(() -> System.out.println("Done"));

Note that if any of the tasks complete with an exception, the returned CompletableFuture will as well.