What are the common misconceptions about ordering and thread assignment when chaining multiple *Async stages on a CompletableFuture?
answer
- Dependency order: guaranteed (+ happens-before)
- Thread identity across *Async: NOT guaranteed
- Each *Async = fresh task = possible new thread
- Siblings on same future: concurrent, any order
- allOf/anyOf input order: arbitrary
basics
~20 sDependent stages still run in order — each waits for the previous result. But *Async does not guarantee the same thread for each stage, and sibling stages attached to the same future can run concurrently in any order.
solid answer
~40 sTwo myths recur. First, that *Async breaks ordering: it does not. A linear chain (A.thenApplyAsync(...).thenApplyAsync(...)) is still strictly ordered because each dependent stage only starts after its predecessor's result exists — *Async changes the thread, not the happens-before dependency. Second, that consecutive *Async stages reuse one thread: they don't. Each *Async submits an independent task to the executor, so stage 2 may run on a different common-pool worker than stage 1, with a hand-off (and memory barrier) between them. Real non-determinism appears with siblings: if you attach two callbacks to the same future, both become eligible when it completes and may run concurrently in unspecified order. Also, completion-order across independent futures (e.g. inside allOf) is arbitrary. So: dependency order is guaranteed; thread identity and sibling/independent ordering are not.
go deeper
Knows a chain runs in order and *Async 'uses other threads,' but may not distinguish dependency order from thread/sibling order.
States that linear chains preserve order while *Async changes the thread, and that each *Async is a fresh task with a possible thread change.
Identifies sibling concurrency and arbitrary allOf/anyOf completion order as real race surfaces, and avoids ThreadLocal/thread-identity assumptions across stages.
Designs pipelines whose correctness rests only on dependency edges, reviews for hidden sibling races, and sets conventions for context propagation (no ThreadLocal reliance) across async boundaries.
## The guarantee that DOES hold: dependency ordering A `CompletableFuture` pipeline is a **dependency graph**. A *dependent* stage (the one you create with `thenApply`, `thenCompose`, etc.) cannot start until the stage it depends on has produced a result. This is true **regardless of async-ness**: ``` A.thenApplyAsync(f).thenApplyAsync(g) ``` Here `g` strictly runs after `f`, because `g`'s stage depends on `f`'s result. `*Async` only changes **which thread** runs `f` and `g`; it does **not** loosen this ordering. There is also a **happens-before** relationship: whatever `f` wrote is visible to `g`, even across different threads, because the completion of one stage *happens-before* the start of the dependent stage. ## Myth 1: '*Async makes stages run out of order' False for a **linear chain**. The data dependency forces order. People sometimes see interleaved log lines from *unrelated* pipelines and wrongly conclude their own chain reordered. Within one dependency chain, order is guaranteed. ## Myth 2: 'consecutive *Async stages run on the same thread' False. **Each `*Async` call submits a separate task** to the executor. After `f` completes on worker-7, stage `g` is submitted afresh and may be picked up by worker-3. There is a **thread hand-off** between every `*Async` stage. (By contrast, a chain of *non-async* stages will tend to run on the **same** completing thread, cascading inline — but that's the non-async behavior, the opposite of what this myth assumes.) Practical consequences: - Don't rely on `ThreadLocal` surviving across `*Async` boundaries — a different thread runs the next stage and won't see the previous thread's thread-local. - A chain of many `*Async` stages incurs a context switch **per stage**; if the stages are tiny and CPU-bound, that overhead can dominate. Mixing in non-async stages avoids needless hand-offs. ## Myth 3: 'siblings run in attach order' False. If you attach **multiple dependents to the same future**: ``` CompletableFuture<X> base = ...; base.thenAcceptAsync(this::a); base.thenAcceptAsync(this::b); ``` when `base` completes, **both `a` and `b` become eligible at once** and may run **concurrently, on different threads, in unspecified order**. There is no guarantee `a` runs before `b`. If `a` and `b` touch shared state, you have a real race. ## Myth 4: 'allOf/anyOf imply an order among the inputs' False. `allOf(...)` only completes when **all** inputs complete; the inputs themselves complete in whatever order their work finishes, on whatever threads — **arbitrary**. `anyOf` completes with the **first** to finish, also non-deterministic. The downstream callback of `allOf` runs (per the async/non-async rule) on the thread that completed *last*, or on the caller if all were already done. ## Myth 5: 'non-async means it runs immediately/eagerly' False. A non-async callback on an **incomplete** upstream still waits for completion; it just then runs on the completing thread. 'Non-async' is about *thread choice*, not *eagerness*. ## The crisp summary - **Guaranteed:** within a dependency chain, stages run in dependency order with happens-before visibility. - **NOT guaranteed:** which thread runs a given `*Async` stage; that consecutive `*Async` stages share a thread; the relative order/thread of **sibling** stages attached to the same future; and the completion order of **independent** futures (allOf/anyOf inputs). Design so that correctness depends only on the **dependency edges**, never on thread identity or sibling ordering.
- Why can't you rely on ThreadLocal across *Async stages?Each *Async stage may run on a different pool thread, so a ThreadLocal set in one stage won't be visible in the next; values must be passed through the pipeline's return values instead.
- If two callbacks are attached to the same future and both mutate shared state, what's the risk?A data race — when the future completes both become eligible and may run concurrently on different threads in unspecified order, so shared mutable state needs synchronization or must be avoided.
saying these in an interview costs you the question
- Claiming *Async reorders a linear dependency chain — dependency order is always preserved.
- Assuming consecutive *Async stages run on the same thread — each submits a fresh task.
- Assuming siblings attached to one future run in attach order — they may run concurrently in any order.
- Assuming allOf imposes an order on its inputs — input completion order is arbitrary.
- Equating 'non-async' with 'runs eagerly/immediately' — it still waits for the upstream.