Search code examples
javaconcurrencydeadlockcompletable-future

How to avoid deadlock when creating multiple interdependent futures from a single CompletableFuture?


Consider the following code:

CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    return 1;
});

// eagerly start this work as soon as f1 completes
CompletableFuture<Integer> f2 = f1.thenApply(x -> 2);

// do work on the main thread before creating another future...
int otherWorkDoneOnMainThread = 3;

CompletableFuture<Integer> f3 = f1.thenApply(x -> {
    f2.join();
    return otherWorkDoneOnMainThread;
});

Thread.sleep(200);
System.out.println(List.of(f1, f2, f3));

This outputs [java.util.concurrent.CompletableFuture@6749fe50[Completed normally], java.util.concurrent.CompletableFuture@52bf7bf6[Not completed, 1 dependents], java.util.concurrent.CompletableFuture@261db982[Not completed]]. Neither f1 nor f2 is completed, because when f1 is completed it first tries to complete f3, which then blocks on f2. The completing thread never gets to f2 because it is waiting on f3 to complete.

Changing f3 be created by f1.thenApplyAsync(...) or f1.thenCombine(f2, ...) solves this, however I'm not sure if that is guaranteed by the API or merely an implementation detail which could change in a later release.

How can I confidently create multiple interdependent futures from a single future?


Solution

  • thenCombine() fixes the problem because your f2 will be guaranteed to be completed before the lambda with f2.join() is called. In fact, you wouldn’t even need to call join() in that case since you would receive the result as input.

    thenApplyAsync() might not fix it depending on the executor being used – a deadlock by thread starvation could still occur.

    I think a basic rule of thumb would be to avoid calling join() from within a CompletableFuture task if you cannot guarantee that it won’t block (e.g. calling it from within the descendants of an allOf() is fine). It should always be possible to use thenCombine()/thenCompose() to avoid the call.