What are the main ways to create a CompletableFuture that runs a task asynchronously, and how do supplyAsync and runAsync differ?
answer
- supply = Supplier = value; run = Runnable = Void
- completedFuture = already done, no task
- new CompletableFuture<>() + complete() = manual
- default executor = common ForkJoinPool
- every async method has an Executor overload
basics
~10 sUse CompletableFuture.supplyAsync when the task returns a value, and runAsync when it returns nothing. supplyAsync takes a Supplier and gives a CompletableFuture<T>; runAsync takes a Runnable and gives a CompletableFuture<Void>.
solid answer
~40 sThere are three common creation paths. CompletableFuture.completedFuture(value) builds one that is already finished with a known value—no task runs. CompletableFuture.supplyAsync(Supplier) runs a value-producing task on a background thread and returns a CompletableFuture<T> that completes with the supplier's result. CompletableFuture.runAsync(Runnable) runs a side-effecting task with no result and returns a CompletableFuture<Void>. The key distinction is the return: supplyAsync's Supplier produces a value you can later transform with thenApply etc.; runAsync's Runnable produces nothing, so you only chain thenRun or thenAccept-style steps that don't need an input. Both run on the common ForkJoinPool by default, but each has an overload taking a custom Executor. You can also create an empty new CompletableFuture<>() and complete it manually later.
go deeper
Knows supplyAsync returns a value (Supplier) and runAsync returns nothing (Runnable), and can pick the right one.
Adds completedFuture and manual new+complete; explains the CompletableFuture<Void> consequence for chaining (thenRun vs thenApply).
Notes the default common ForkJoinPool and the Executor overloads, and why blocking tasks need a dedicated executor.
Reasons about pool sizing/starvation across the JVM, async API contracts (when to expose a CompletableFuture-returning method), and bridging external callbacks via manual completion.
## What a CompletableFuture is A `Future<T>` in Java represents a value that will be available *later*—the result of a computation running, typically, on another thread. The original `java.util.concurrent.Future` (Java 5) was limited: you could only `get()` (which blocks) or `cancel()`. You could not say "when this finishes, do that" without blocking a thread. `CompletableFuture<T>` (Java 8, in `java.util.concurrent`) fixes this. It implements `Future<T>` *and* `CompletionStage<T>`. "Completion stage" means you can attach **callbacks** that fire when it completes, building a non-blocking pipeline (`thenApply`, `thenCompose`, etc.). This question is only about how you **create** one. ## Term definitions - **Supplier<T>**: a functional interface with one method `T get()`—takes nothing, returns a value. Written as a lambda `() -> computeSomething()`. - **Runnable**: a functional interface with `void run()`—takes nothing, returns nothing (a side effect only). - **Executor**: an object that runs submitted tasks. It decides *which thread* runs the work. - **ForkJoinPool.commonPool()**: a shared, JVM-wide thread pool sized by default to (CPU cores − 1) threads. It is the *default* executor for `CompletableFuture`'s async methods. ## The creation methods ### 1. `completedFuture(value)` ```java CompletableFuture<String> cf = CompletableFuture.completedFuture("hello"); ``` No task runs. The future is born *already complete* with the given value. `cf.get()` returns immediately. Useful for returning a constant from a method whose signature is async, or as a starting point in tests. ### 2. `supplyAsync(Supplier<T>)` — produces a result ```java CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 2 + 2); ``` The supplier runs on a background thread (the common pool by default). When it returns, the future completes with that value (here `4`). Because there is a result, you can transform it: `cf.thenApply(x -> x * 10)`. ### 3. `runAsync(Runnable)` — no result ```java CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> log.info("done")); ``` The runnable runs on a background thread but produces nothing, so the type is `CompletableFuture<Void>`. You chain steps that don't need an input value: `cf.thenRun(() -> ...)`. There is no value to feed into a `thenApply`. ### 4. Manual completion ```java CompletableFuture<String> cf = new CompletableFuture<>(); // ... later, from any thread/callback: cf.complete("value"); // or cf.completeExceptionally(ex) ``` You create an *empty, incomplete* future and complete it yourself when an external event (a callback, a message, a timer) arrives. This is the bridge between callback-style APIs and the CompletableFuture world. ## supplyAsync vs runAsync — the core difference | | `supplyAsync` | `runAsync` | |---|---|---| | Argument | `Supplier<T>` | `Runnable` | | Result type | `CompletableFuture<T>` | `CompletableFuture<Void>` | | Has a value? | Yes | No | | Next steps | `thenApply`, `thenCompose`… | `thenRun` | Choose by whether the work *yields a value you need downstream* (supply) or is a *fire-and-forget side effect* (run). ## Executor selection Every async creator has two overloads: ```java supplyAsync(supplier) // common ForkJoinPool supplyAsync(supplier, myExecutor) // your pool ``` The common pool is fine for short CPU-bound work but is **shared across the whole JVM**; long or blocking tasks (I/O, JDBC, sleeps) can starve it and stall unrelated code. For blocking work, pass a dedicated `Executor`.
- After runAsync, why can't you call thenApply on the result?runAsync yields a CompletableFuture<Void>—there is no value to transform. thenApply needs an input value, so you use thenRun (no input) instead.
- When would you use completedFuture instead of supplyAsync?When the value is already known and no computation is needed—e.g. returning a cached/constant result from a method whose signature must be async, or seeding a pipeline/test.
saying these in an interview costs you the question
- Saying runAsync returns a value or takes a Supplier
- Thinking supplyAsync blocks the calling thread until the result is ready
- Believing each async call spins up a brand-new thread rather than using a shared pool
- Confusing completedFuture (no task) with supplyAsync (runs a task)