multi threading
advancedCompletableFuture
Compose asynchronous computations into pipelines — like Promises in JavaScript — without blocking on every step.
A CompletableFuture stands for a result that has not arrived yet — the answer to a database query or an HTTP call that is still in flight.
Pizza ordering app — track each pizza's status independently. "When all three are ready, send the delivery driver." No staring at a single oven.
Key Concepts
1
Instead of blocking a thread while you wait, you say what should happen when it arrives.
2
fetchUser(id) // returns immediately
.thenApply(User::getEmail) // when the user arrives, take the email
.thenAccept(this::send); // then send to it3
No thread sits idle during any of that.
4
Why it replaced Future. The older Future could only be asked "are you done?" or "wait for you".
You could not say what to do next, combine two of them, or attach error handling. CompletableFuture can
do all three.
FutureCompletableFuture
5
The method names follow a pattern. Once you see it, the large API shrinks:
6
thenApply(fn) the function returns a value -> transform
thenAccept(fn) the function returns nothing -> consume
thenRun(r) ignores the value entirely
thenCompose(fn) the function returns ANOTHER future -> flatten
thenCombine(o,fn) wait for two futures, combine them7
thenApply versus thenCompose is the one to get right. If your function itself returns a
CompletableFuture and you use thenApply, you end up with a future inside a future:
thenApplythenComposeCompletableFuture
8
fetchUser(id).thenApply(u -> fetchOrders(u)) // CF<CF<List<Order>>>
fetchUser(id).thenCompose(u -> fetchOrders(u)) // CF<List<Order>>9
Which thread runs your code is the trap. Without the Async suffix, the next step runs on whichever
thread finished the previous one. That might be a library's internal networking thread, and doing slow
work there blocks something you do not own.
Async
10
With Async and no executor, it runs on a pool that is shared by the whole JVM and sized for computation,
not waiting. Pass your own executor for anything that blocks.
Async
11
Running calls in parallel, done correctly.
// sequential by accident -- each join waits before the next starts
for (var id : ids) results.add(fetch(id).join());12
// actually parallel -- all started, then waited for
var futures = ids.stream().map(this::fetch).toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
var users = futures.stream().map(CompletableFuture::join).toList();13
The second join does not block, because those futures are already finished.
join
14
Handling failure. Three tools, with different shapes:
15
exceptionally(fn) recover: supply a fallback value
handle(fn) runs either way, receives value and error
whenComplete(fn) observe both, change nothing16
Exceptions arrive wrapped in CompletionException, so the real cause is one getCause() away. And a chain
that nobody ever waits on can swallow its failure entirely — always finish with a join, a handle, or a
whenComplete.
CompletionExceptiongetCause()joinhandlewhenComplete
17
What the interviewer is probing.1. "Why this over Future?" Probing: what it adds. Stalls: "It is newer." Moves up: chaining,
combining and error handling without a thread waiting.
Future
18
2. "thenApply or thenCompose?" Probing: the flattening rule. Stalls: guessing. Moves up:
thenCompose when the function itself returns a future, or you get a nested one.
thenApplythenCompose
19
3. "Which thread runs the callback?" Probing: the trap. Stalls: "A pool thread." Moves up:
without Async, whichever thread completed the previous step — possibly a library's.
Async
20
4. "How do you run ten calls in parallel?" Probing: whether your loop is actually sequential.
Stalls: a loop that joins each one. Moves up: start them all, then allOf, then collect.
allOf
21
5. "How do you handle an error?" Probing: the three tools. Stalls: "try/catch." Moves up:
exceptionally to recover, handle for both paths, and unwrap CompletionException.
exceptionallyhandleCompletionException
22
The method grid23
value in -> returns
------------------------------------------------------------------
thenApply(fn) T -> R CF<R> transform
thenAccept(c) T -> void CF<Void> consume
thenRun(r) () -> void CF<Void> ignore the value
thenCompose(fn) T -> CF<R> CF<R> flatten (flatMap)
thenCombine(o,fn) (T,U) -> R CF<R> join two stages
exceptionally(fn) Throwable -> T CF<T> recover
handle(fn) (T,Throwable)->R CF<R> both paths
whenComplete(c) (T,Throwable) CF<T> observe, unchanged24
Each has an ...Async form: same semantics, different thread.25
Parallel calls, done correctly26
// sequential by accident - each join blocks before the next starts
for (var id : ids) results.add(fetch(id).join());27
// parallel
List<CompletableFuture<User>> futures =
ids.stream().map(this::fetch).toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.join();
List<User> users = futures.stream()
.map(CompletableFuture::join) // done
.toList();28
Timeouts (Java 9+)
orTimeout(3, SECONDS) fail after 3 s
completeOnTimeout(fallback, 3, SECONDS) default after 3 s29
Thread rules
no Async -> runs on the completing thread (or the caller,
if already complete)
Async, no exec-> common ForkJoinPool, shared process-wide,
sized for CPU work
Async + exec -> your pool <- prefer this for anything blocking