Skip to content

Expert · Multithreading and concurrency · topic 43 of 60 · 30 min

Executors, futures and CompletableFuture

Explanation

Instead of creating threads by hand, submit tasks to an ExecutorService (a thread pool) that reuses a fixed number of threads. submit returns a Future whose get() waits for the result. CompletableFuture chains asynchronous steps (supplyAsync, thenApply, thenCombine, exceptionally) without blocking, and allOf waits for many. Always shut pools down.

Why thread pools?

Creating a thread costs time and memory. A server that started a new platform thread for every request would collapse under load. A thread pool keeps a set of worker threads alive and feeds them tasks from a queue. The Executors class creates common pools.

java
ExecutorService pool = Executors.newFixedThreadPool(4);   // 4 reusable worker threads

Future<Double> gst = pool.submit(() -> calculateGst(orders));     // a Callable returns a value
Future<Integer> stock = pool.submit(() -> countStock(warehouse));

double tax = gst.get();          // waits if not done yet (throws InterruptedException, ExecutionException)
int units = stock.get(2, TimeUnit.SECONDS);   // wait at most 2 s, else TimeoutException

pool.shutdown();                 // accept no new tasks; finish queued ones
pool.awaitTermination(30, TimeUnit.SECONDS);

Runnable is a task with no result (void run()); Callable<V> returns a value and may throw checked exceptions (V call()). If the task throws, Future.get throws ExecutionException with the original as its cause.

Careful: Forgetting shutdown() leaves non-daemon pool threads running, so the JVM never exits. Since Java 19, ExecutorService is AutoCloseable, so try (var pool = Executors.newFixedThreadPool(4)) { … } shuts it down and waits automatically. Also avoid Executors.newCachedThreadPool for unbounded load: it can create thousands of threads.

CompletableFuture: chaining async steps

Future.get() blocks the calling thread. CompletableFuture (Java 8) lets you describe what should happen when results arrive, combine several results, and handle errors, all without blocking until the very end.

java
CompletableFuture<Customer> customer = CompletableFuture.supplyAsync(() -> crm.find(id));
CompletableFuture<List<Order>> orders = CompletableFuture.supplyAsync(() -> orderRepo.forCustomer(id));

CompletableFuture<String> summary = customer
        .thenCombine(orders, (c, os) -> c.name() + " has " + os.size() + " orders")   // when BOTH are done
        .thenApply(String::toUpperCase)                                                // transform
        .exceptionally(ex -> "Summary unavailable: " + ex.getMessage());              // on any failure

System.out.println(summary.join());   // join() waits; like get() but throws unchecked exceptions

CompletableFuture<Void> all = CompletableFuture.allOf(customer, orders);   // wait for many
CompletableFuture<String> withTimeout = summary.orTimeout(2, TimeUnit.SECONDS);   // Java 9+
MethodDoes
supplyAsync(supplier)Run a task that returns a value on a pool (the common ForkJoinPool by default)
thenApply(fn)Transform the result
thenCompose(fn)Chain another async step that returns a CompletableFuture
thenCombine(other, fn)Combine two independent results
exceptionally / handleRecover from errors
allOf / anyOfWait for all / the first of many
join() / get()Wait for the final value
Think of it like this: A thread pool is like a team of four delivery riders at a restaurant: orders (tasks) queue up and whichever rider is free takes the next one, instead of hiring a new rider for every order. A CompletableFuture is like the order-tracking app: "when the food is packed, then assign a rider, then notify the customer", without anyone standing and waiting at each step.
Tip: Size CPU-bound pools near the number of cores (Runtime.getRuntime().availableProcessors()); I/O-bound work (waiting on databases and APIs) benefits from more concurrency, which is exactly what virtual threads (topic 5) make cheap.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Executors, futures and CompletableFuture · 1/6

Executors and futures

In this lesson you will run tasks with thread pools and futures.

Prefer a video by a teacher? Find free videos on “java executorservice future completablefuture tutorial” on YouTube ↗

In real life: Loading a travel search page

A travel site's search API calls three partner services (trains, buses, flights) for the same route. Calling them one after another takes the sum of their times; launching them as CompletableFutures and combining the results with allOf takes only as long as the slowest, with a timeout so one slow partner cannot hold up the page.

java
var trains = CompletableFuture.supplyAsync(() -> trainApi.search(route)).orTimeout(3, TimeUnit.SECONDS);
var buses = CompletableFuture.supplyAsync(() -> busApi.search(route)).orTimeout(3, TimeUnit.SECONDS);
CompletableFuture.allOf(trains, buses)
        .exceptionally(ex -> null)          // keep going even if one partner fails
        .join();
List<Option> results = new ArrayList<>();
if (!trains.isCompletedExceptionally()) results.addAll(trains.join());
if (!buses.isCompletedExceptionally()) results.addAll(buses.join());

Practice (0/3 done)

Task 1 · Question

A task submitted to an ExecutorService throws an exception. What does future.get() do?

Task 2 · Code · Javascript

The same "combine two async results" idea in JavaScript, which runs here (promises are JavaScript's CompletableFuture): write summary(findCustomer, findOrders, id) that starts both async calls at the same time and returns "<NAME IN CAPITALS> has <n> orders", or "Summary unavailable" if either fails.

Task 3 · Practice

Write a program that uses an ExecutorService with 3 threads (in try-with-resources, Java 19+, or with shutdown) to compute the total sales of 5 shops in parallel (each task sleeps 500 ms to simulate a database call), collects the 5 Futures, and prints the grand total and the time taken (about 1 second, not 2.5).

Quick quiz

Question 1

1.What is the difference between Runnable and Callable?

Question 2

2.What happens if you never call shutdown() on a fixed thread pool in main?

Question 3

3.Which CompletableFuture method combines two independent results?

Question 4

4.What does exceptionally do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the difference between Runnable and Callable?

Answer: Callable returns a value and can throw checked exceptions

V call() throws Exception vs void run().

What happens if you never call shutdown() on a fixed thread pool in main?

Answer: The JVM may keep running because the pool threads are alive

Pool threads are non-daemon.

Which CompletableFuture method combines two independent results?

Answer: thenCombine

thenCombine runs a function when both are complete.

What does exceptionally do?

Answer: Provides a fallback value when an earlier stage failed

It recovers from errors in the chain.

Where does an error from an awaited Future appear?Related

Answer: At the await, where try/catch can handle it

It is rethrown at the await.

From Futures and async/await

Two bank balances must be updated together by several threads. Which tool?Related

Answer: One std::mutex locked around both updates

Only a lock makes the combined change appear all at once.

From Concurrency 2: atomics, async, futures and condition variables

What is the danger of a long CPU loop inside an async task?Related

Answer: It blocks the worker thread, so other tasks stall

Tasks must yield at .await points; use spawn_blocking for heavy work.

From Async Rust with tokio

Why avoid task.Result on an unfinished task?Related

Answer: It blocks the thread and can deadlock in some apps

Blocking defeats the purpose of async and can deadlock.

From async and await: waiting without blocking

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Multithreading and concurrency” before its exam: Atomic variables and concurrent collections, Virtual threads (Java 21).
  2. Module 9 exam: Multithreading and concurrency · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, JDBC, Maven, Gradle and JUnit. 60 topics left in the course before the final exam.

← Previous: Race conditions and synchronized