Explanation
What just happened
main described a three-step pipeline and then waited on join(). A common-pool thread ran the steps in order: fetchUser, then thenApply with the user, then thenAccept with the name. Each step started the moment the previous one finished, with its result.
supplyAsync(() -> ...): run this on another thread and produce a value.thenApply(x -> ...): turn the value into a new value.thenAccept(x -> ...): use the value, return nothing.
No thread waits between steps. main only waits at the very end, because this program has nothing else to do.
Try the what-if switch
thenApply vs thenApplyAsync: the same parse callback runs on three different threads.
- main, for
done.thenApply(...):donewas already complete when the callback was attached. - io-1, for
f.thenApply(...): io-1 is the thread that completedf. - cpu-1, for
f.thenApplyAsync(..., cpuPool): async means “submit to this executor”.
The rule
- Non-async (
thenApply,thenAccept, …): run wherever, right now. That’s the thread that completes the future, or the caller if it’s already done. - Async (
thenApplyAsync(fn, executor)): always submit to the executor. Without one, that means the common pool.
Watch out
You don’t control where a plain thenApply runs. Put heavy work there and it might run on an HTTP client’s I/O thread. Use thenApplyAsync with an executor you own for slow or blocking work.
supplyAsync without an executor uses the shared common pool, whose threads are daemons. If main exits first, the pipeline may never finish.
Common questions
Which thread runs thenApply?
The thread that completes the future. If the future is already complete when you call thenApply, the calling thread runs the callback at once.
Which thread runs thenApplyAsync?
A thread of the executor you pass. Without one, it uses ForkJoinPool.commonPool(). It doesn't inherit the executor of the stage before it.
Why does my CompletableFuture print nothing?
supplyAsync without an executor runs on the common pool, whose threads are daemon threads. If main ends first, the JVM can exit before the pipeline finishes. Call join() or pass your own executor.
Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us