Multithreading
CompletableFuture
Write asynchronous, non-blocking pipeline operations with CompletableFuture.
Interview: Commonly tested on chaining operations (thenApply, thenAccept, thenCompose, thenCombine) and error handling.
Introduced in Java 8, CompletableFuture implements Future and CompletionStage, enabling functional-style, event-driven asynchronous programming. It allows chaining operations and handling errors without blocking threads.
Core Idea
CompletableFuture enables non-blocking task chains, reactive combination, and declarative error handling.
Why It Matters
It eliminates blocking Future.get() calls, freeing up CPU worker threads to handle other incoming workloads.
Interview Lens
Expect design scenarios: write an asynchronous workflow combining two microservice call futures.
Core Chaining API Methods
These methods construct non-blocking pipelines:
thenApply(Function): Transforms the result of the previous stage (analogous tomap).thenAccept(Consumer): Consumes the final result without returning a value (ends the pipeline).thenCompose(Function): Flattens nested futures (analogous toflatMap). Use this if your transform function returns a newCompletableFuture.thenCombine(CompletionStage, BiFunction): Combines two independent futures concurrently, running a function when both complete.exceptionally(Function): Intercepts errors thrown anywhere in the chain and recovers with a fallback value.
Code Walkthrough
This program simulates an async pipeline fetching user IDs, retrieving email data, and recovering from errors.
import java.util.concurrent.CompletableFuture;public class CompletableFutureDemo { public static void main(String[] args) throws InterruptedException { // Start async task using default ForkJoinPool CompletableFuture.supplyAsync(() -> { System.out.println("Fetching user ID..."); return "user_123"; }) // Chain a dependent transformation .thenApply(userId -> { System.out.println("Constructing email..."); return userId + "@example.com"; }) // Handle potential errors .exceptionally(throwable -> { return "fallback@example.com"; }) // Consume final value .thenAccept(email -> { System.out.println("Sending email to: " + email); });
Thread.sleep(1000); // Wait for async task execution to complete } }
Interview-Relevant Information
Q: Which threads execute CompletableFuture stages?
Answer: By default, tasks started with supplyAsync run in the common JVM ForkJoinPool.commonPool(). For chaining operations (like thenApply), if the previous stage completes quickly, it may run on the thread that completed it or the caller thread. Methods with the *Async suffix (e.g. thenApplyAsync) always submit execution to the common ForkJoinPool or a custom executor if provided.
Quick Checklist
How do thenApply and thenCompose differ? What thread pool does supplyAsync use by default? If yes, you are ready to write reactive Java code.
Use Cases
Orchestrating microservice call pipelines in high-throughput API gateways.
Asynchronously retrieving configurations from multiple servers concurrently.
Common Mistakes
Forgetting to wait for active async tasks in CLI tools, causing the main thread to terminate the JVM before tasks complete.
Not catching exceptions inside async tasks, causing silent failures in the pipeline.