Backend Development 13 min read

Deep Dive into Java CompletableFuture: Implementation, Usage, and Internal Mechanics

This article provides an in‑depth analysis of Java’s CompletableFuture introduced in JDK 8, explaining its motivations, core methods, internal mechanisms such as asyncSupplyStage, thenAcceptAsync, and postComplete, and includes practical code examples to illustrate asynchronous task handling and dependency management.

Top Architect
Top Architect
Top Architect
Deep Dive into Java CompletableFuture: Implementation, Usage, and Internal Mechanics

CompletableFuture, introduced in JDK 1.8, extends Future and CompletionStage to enable asynchronous callbacks and chainable operations without blocking.

Why CompletableFuture? The older Future in JDK 1.5 required blocking or polling to obtain results, which is inefficient. CompletableFuture registers callbacks, allowing observers to be notified when a task completes, similar to Netty's ChannelFuture.

Key Functions

Conversion (thenCompose)

Combination (thenCombine)

Consumption (thenAccept)

Execution (thenRun)

Consumption with return (thenApply)

Consumption uses the result of a previous stage, while execution runs a task without needing the result.

CompletableFuture can perform chain calls via CompletionStage methods, choosing synchronous or asynchronous execution.

Below is a simple example demonstrating these capabilities:

public static void thenApply() {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
        try {
            // Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("supplyAsync " + Thread.currentThread().getName());
        return "hello";
    }, executorService).thenApplyAsync(s -> {
        System.out.println(s + "world");
        return "hhh";
    }, executorService);
    cf.thenRunAsync(() -> {
        System.out.println("ddddd");
    });
    cf.thenRun(() -> {
        System.out.println("ddddsd");
    });
    cf.thenRun(() -> {
        System.out.println(Thread.currentThread());
        System.out.println("dddaewdd");
    });
}

Execution result (order may vary depending on sync/async choice):

supplyAsync pool-1-thread-1
helloworld
ddddd
ddddsd
Thread[main,5,main]
dddaewdd

Key observations:

If thenRun is synchronous, it may run on the main thread or the thread that completed the source task.

When multiple dependent tasks exist, synchronous tasks executed by the same thread follow LIFO order, while asynchronous tasks are scheduled in a thread pool without guaranteed order.

Source Code Tracing

Creating CompletableFuture

Various factory methods exist; supplyAsync creates a CompletableFuture that runs a Supplier on a given Executor (or the common ForkJoinPool if none is provided).

public static
CompletableFuture
supplyAsync(Supplier
supplier, Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

static Executor screenExecutor(Executor e) {
    if (!useCommonPool && e == ForkJoinPool.commonPool())
        return asyncPool;
    if (e == null) throw new NullPointerException();
    return e;
}

asyncSupplyStage creates a new CompletableFuture, wraps the supplier in an AsyncSupply task, and submits it to the executor.

static
CompletableFuture
asyncSupplyStage(Executor e, Supplier
f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture
d = new CompletableFuture<>();
    e.execute(new AsyncSupply
(d, f));
    return d;
}

AsyncSupply#run obtains the dependent CompletableFuture and its supplier, executes the supplier, and completes the future with the obtained value or exception, then calls postComplete() to trigger dependent stages.

public void run() {
    if ((d = dep) != null && (f = fn) != null) {
        dep = null; fn = null;
        if (d.result == null) {
            try { d.completeValue(f.get()); }
            catch (Throwable ex) { d.completeThrowable(ex); }
        }
        d.postComplete();
    }
}

thenAcceptAsync builds a new dependent CompletableFuture and registers a UniAccept stage that will invoke the consumer when the source completes.

public CompletableFuture
thenAcceptAsync(Consumer
action) {
    return uniAcceptStage(asyncPool, action);
}

private CompletableFuture
uniAcceptStage(Executor e, Consumer
f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture
d = new CompletableFuture<>();
    if (e != null || !d.uniAccept(this, f, null)) {
        UniAccept
c = new UniAccept
(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

The UniAccept stage’s tryFire method attempts to execute the consumer immediately if the source future is already completed; otherwise it is stored in the dependent stack.

final CompletableFuture
tryFire(int mode) {
    if ((d = dep) == null || !d.uniAccept(a = src, fn, mode > 0 ? null : this))
        return null;
    dep = null; src = null; fn = null;
    return d.postFire(a, mode);
}

postComplete iterates over the dependent stack, popping each Completion node and invoking its tryFire method, using a nested mode to avoid recursive loops.

final void postComplete() {
    CompletableFuture
f = this; Completion h;
    while ((h = f.stack) != null || (f != this && (h = (f = this).stack) != null)) {
        CompletableFuture
d; Completion t;
        if (f.casStack(h, t = h.next)) {
            if (t != null) {
                if (f != this) { pushStack(h); continue; }
                h.next = null; // detach
            }
            f = (d = h.tryFire(NESTED)) == null ? this : d;
        }
    }
}

In summary, CompletableFuture’s design relies on a stack of dependent Completion objects; when the source task finishes, postComplete triggers each dependent stage, handling both synchronous and asynchronous execution paths while avoiding unbounded recursion.

The article concludes that understanding these internal mechanisms—especially AsyncSupply , UniAccept , and postComplete —is essential for mastering asynchronous programming in Java.

backendJavaConcurrencyasynchronousCompletableFutureJDK8
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.