Waking up a sleeping thread is a dependent action that must be handled like any other, and it has no priority. On the other hand, the polling flow of a CompletableFuture , when it has already been completed, will not be strewn;
In the next program
public static void main(String[] args) { final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> { waitAndLog("Supplier", null, 1000); throw new RuntimeException("First"); }).thenApply(Function.identity()); long start = System.nanoTime(); CompletableFuture.runAsync(() -> waitAndLog("A", future, 0)); LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10)); future.exceptionally(e -> { waitAndLog("Exceptionally", null, 1000); return null; }); CompletableFuture.runAsync(() -> waitAndLog("B", future, 0)); CompletableFuture.runAsync(() -> waitAndLog("C", future, 1100)); waitAndLog("Main", future, 0); ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS); } private static void waitAndLog(String msg, CompletableFuture<?> primary, int sleep) { long nanoTime = System.nanoTime(); Object result; try { if(sleep>0) Thread.sleep(sleep); result = primary!=null? primary.get(): null; } catch (InterruptedException|ExecutionException ex) { result = ex; } long millis=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-nanoTime); System.out.println(msg+" waited for "+millis+"ms"+(result!=null? ", got "+result: "")); }
I get
Supplier waited for 993ms A waited for 993ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First C waited for 1108ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First Exceptionally waited for 998ms Main waited for 1983ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First B waited for 1984ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
on my machine, assuming that in this particular case the dependent actions were performed in the order in which they were planned, A Note that I added an extra wait time before scheduling Exceptionally , which will be the next dependent action. Since B runs in the background thread, it is not deterministic regardless of whether it manages to plan itself before the Main thread or not. We could insert another delay in order to either complete the order.
Since C checks for an already completed future, it can act immediately, so its network latency is close to the explicit sleep time.
It should be emphasized that this is only the result of a specific scenario, depending on implementation details. There is no guarantee order of execution for dependent activities. As you may have noticed, without the .thenApply(Function.identity()) step, the implementation executes a different code path leading to a different order of execution of dependent actions.
Dependencies form a tree, and the implementation should efficiently move it without risking a stack overflow, therefore, it should somehow smooth it, and small changes in the shape of the dependency tree can affect the resulting order in an unintuitive way.