Search code examples
javamultithreadingjava.util.concurrentcompletable-future

Why does this CompletableFuture work even when I don't call get() or join()?


I had a question while studying CompletableFuture. The get()/join() methods are blocking calls. What if I don't call either of them?

This code calls get():

// Case 1 - Use get()
CompletableFuture.runAsync(() -> {
    try {
        Thread.sleep(1_000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("Hello");
}).get();
System.out.println("World!");

Thread.sleep(5_000L); // Don't finish the main thread

Output:

Hello
World!

This code calls neither get() nor join():

// Case 2 - Don't use get()
CompletableFuture.runAsync(() -> {
    try {
        Thread.sleep(1_000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("Hello");
});
System.out.println("World!");

Thread.sleep(5_000L); // For don't finish main thread

Output:

World!
Hello

I don't know why the runnable block of case 2 is working.


Solution

  • The entire idea of CompletableFuture is that they are immediately scheduled to be started (though you can't reliably tell in which thread they will execute), and by the time you reach get or join, the result might already be ready, i.e.: the CompletableFuture might already be completed. Internally, as soon as a certain stage in the pipeline is ready, that particular CompletableFuture will be set to completed. For example:

    String result = 
       CompletableFuture.supplyAsync(() -> "ab")
                        .thenApply(String::toUpperCase)
                        .thenApply(x -> x.substring(1))
                        .join();
    

    is the same thing as:

    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "ab");
    CompletableFuture<String> cf2 = cf1.thenApply(String::toUpperCase);
    CompletableFuture<String> cf3 = cf2.thenApply(x -> x.substring(1));
    String result = cf3.join();
    

    By the time you reach to actually invoke join, cf3 might already finish. get and join just block until all the stages are done, it does not trigger the computation; the computation is scheduled immediately.


    A minor addition is that you can complete a CompletableFuture without waiting for the execution of the pipelines to finish: like complete, completeExceptionally, obtrudeValue (this one sets it even if it was already completed), obtrudeException or cancel. Here is an interesting example:

     public static void main(String[] args) {
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            System.out.println("started work");
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            System.out.println("done work");
            return "a";
        });
    
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        cf.complete("b");
        System.out.println(cf.join());
    }
    

    This will output:

    started work
    b
    

    So even if the work started, the final value is b, not a.