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

I don't know why the Runnable block of case2 is working.

There is no reason why it would NOT work.

The runAsync(...) method says to do a task asynchronously. Assuming that the application doesn't end prematurely the task will be done eventually, whether you wait for it to be done or not.

The CompletableFuture provides various ways of waiting for the task to complete. But in your example, you are not using it for that purpose. Instead, the Thread.sleep(...) call in your main method is having the same effect; i.e. it is waiting long enough that the task has (probably) finished. So "Hello" is output before "World".

Just to reiterate, the get() call doesn't cause the task to happen. Rather it waits for it to have happened.


Using sleep to wait for an event (e.g. completion of a task) to happen is a bad idea:

  1. Sleep doesn't tell if the event has happened!
  2. You typically don't know exactly how long it will take for the event to happen, you don't know how long to sleep.
  3. If you sleep too long you have "dead time" (see below).
  4. If you don't sleep long enough, the event may not have happened yet. So you need to test and sleep again, and again, and ...

Even in this example, it is theoretically possible1 for the sleep in main to finish before the sleep in the task.

Basically, the purpose of the CompletableFuture is to provide an efficient way to wait for a task to finish and deliver a result. You should use it ...

To illustrate. Your application is waiting (and wasting) ~4 seconds between outputting "Hello" and "World!". If you used the CompletableFuture as it is intended to be used, you wouldn't have those 4 seconds of "dead time".


1 - For example, some external agent might be able to selectively "pause" the thread that is running the task. It might be done by setting a breakpoint ...


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.


The second case is "working" because you sleep the main thread long enough (5 seconds). Working is between quotes because it isn't really working, just finishing. I'm assuming here the code should output Hello World! in order to be considered "working properly".


Try the same code with this sleep time at the end of the main thread in both cases:

Thread.sleep(100);

1. The first one would behave in the same way, as the get operation blocks the main thread. In fact, for the first case, you don't even need the last sleep time.

Output: Hello World!


2. The second case won't output Hello, as no one told the main thread: "hey, wait for this to finish". That's what get() does: block the caller in order to wait for the task to finish. Without it, and setting a low sleep time at the end, the runnable is called, but couldn't finish its job before the main thread stops.

Output: World!


That's also the reason why in the first case Hello World! (first the runnable's output, and then main's one- meaning the main thread was blocked until get() returned) is written, while the second one shows subtle signs of dyslexia: World Hello!

But it's not dyslexic, it just executes what it is told to. In the second case, this happens:

1. The runnable is called.

2. Main thread continues its process, printing ("World!)

3. Sleep times are set: 1 second on the runnable / 5 seconds on main. (runnable's sleep could also be executed during the 2nd step, but I put it here in order to clarify the behaviour)

4. The runnable task prints ("Hello") after 1 second and the CompletableFuture is finished.

5. 5 Seconds passed, main thread stops.

So your runnable could print Hello because it was able to execute the command in between those 5 seconds timeout.

World! . . . . . .(1)Hello. . . . . . . . . . .(5)[END]

If you decrease the last 5 seconds timeout, for example, to 0.5 seconds, you get

World!. . (0.5)[END]