You model the dependencies of the completion stages by linking them. If you associate two actions A and B with another action C , you define the relations A → C and B → C , but have no relationship between A and B and therefore there is no relation, in other words, you cannot even assume that one will work after another, i.e.
CompletableFuture<String> base=CompletableFuture.supplyAsync(() -> { LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2)); return "source"; }); base.thenAccept(s -> { System.out.println("entered first consumer in "+Thread.currentThread()); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); System.out.println("leaving first consumer"); }); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2)); base.thenAccept(s -> { System.out.println("entered second consumer in "+Thread.currentThread()); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); System.out.println("leaving second consumer"); });
most likely will print something like
entered first consumer in Thread[ForkJoinPool.commonPool-worker-1,5,main] entered second consumer in Thread[main,5,main] leaving second consumer leaving first consumer
Although, of course, there is no guarantee about this.
To ensure dependency between two consumers, you must appropriately link them, for example.
CompletableFuture<String> base=CompletableFuture.supplyAsync(() -> { LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2)); return "source"; }); CompletableFuture<Void> next = base.thenAccept(s -> { System.out.println("entered first consumer in "+Thread.currentThread()); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); System.out.println("leaving first consumer"); }); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2)); base.thenAcceptBoth(next, (s,ignored) -> { System.out.println("entered second consumer in "+Thread.currentThread()); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); System.out.println("leaving second consumer"); }).join();
Here, the second consumer is tied to base and next to get the result from base , but depends on the completion of next s (which you would normally not require if there was no result for the transfer - perhaps you want to rethink your design if you have such a scenario) .
Alternatively, you can convert the first Consumer to Function that passes through this value, so you can bind it with thenApply to bind another thenAccept stage to thenAccept .