24

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.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
SangHoon Lee
  • 243
  • 2
  • 5
  • 3
    Why wouldn't you expect it to get run? – Louis Wasserman Jan 07 '21 at 01:41
  • 2
    @LouisWasserman There was nothing in the material I learned that I didn't write them down. So I expected that it would not work. Like the Terminal Operations of Stream Api. – SangHoon Lee Jan 07 '21 at 01:46
  • 5
    @LouisWasserman With designs like Reactive Streams being commonplace, it's not always obvious to beginners the difference between "push" and "pull" approaches. – chrylis -cautiouslyoptimistic- Jan 07 '21 at 02:01
  • 1
    Great question. Other examples of "pull" behavior: In Python, a generator doesn't actually do anything until it is executed. If you create a generator but don't run it, nothing happens. The same goes for futures in Rust. They only run when you `.await` them. – John Kugelman Jan 07 '21 at 02:02
  • @chrylis-cautiouslyoptimistic- That's right. I didn't understand the "pull" and "push" method. Let's learn about these two concepts. Thank you. – SangHoon Lee Jan 07 '21 at 02:16
  • @JohnKugelman I thought it was done when I called get or join. Thank you for your good answer. – SangHoon Lee Jan 07 '21 at 02:19

3 Answers3

18

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.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 1
    To be more precise, those factory methods will schedule the action or supplier immediately whereas the `CompletionStage` methods will schedule immediately when the prerequisites are fulfilled. In contrast, the idea of the `CompletableFuture` itself is different; as its name suggests, it is just a future that can be completed, i.e. when you construct an instance via the default constructor, nothing will happen unless someone explicitly completes it. The second part of your answer should not forget `cancel`, which is nothing but an exceptional completion. – Holger Jan 12 '21 at 08:17
14

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 ...

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 1
    You don’t need an external agent to selectively pause the thread, having a high system load would already have the same effect; when no thread makes progress, the time will continue to tick, allowing `sleep` to return once the application gets CPU time again. – Holger Jan 12 '21 at 08:21
  • Yes. There are other ways it could happen. – Stephen C Jan 12 '21 at 08:45
8

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]
dreamcrash
  • 47,137
  • 25
  • 94
  • 117
aran
  • 10,978
  • 5
  • 39
  • 69
  • Okay, thank you. Then does the runnable code that used get() or not work? – SangHoon Lee Jan 07 '21 at 01:58
  • @SangHoonLee In the second example, it won't work as it didn't have enough time to finish. get() just guarantees the caller thread to block until the runnable has finished. Without it, is just an async process, so the main thread won't wait for it: it will just call it and continue its process. – aran Jan 07 '21 at 02:00
  • 1
    _Not even the runnable's sleep would be able to finish sleeping_. this is not entirely correct. Just change it to `CompletableFuture.runAsync(() -> { try { Thread.sleep(1_000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Hello"); }, Executors.newSingleThreadExecutor());` and see what happens. – Eugene Jan 07 '21 at 03:28
  • Totally true, wrote that because in this case the async is executed without an specific executor, so it uses the forkJoinPool. But you are right there, so I'll edit that part to avoid confusion – aran Jan 07 '21 at 03:29
  • @Eugene this is a doubt that just hammered me now... how is the "false" async behaviour happen in this cases? Is just a "shallow" thread? – aran Jan 07 '21 at 03:30
  • where does `runAsync` executes _without_ an executor being specified? – Eugene Jan 07 '21 at 03:32
  • *ForkJoinTask is a thread-like entity that is much lighter weight than a normal thread*. But the singleThreadExecutor would create a "real" thread. -->am I going in the correct path? Im intrigued now, seriously – aran Jan 07 '21 at 03:40
  • The forkJoinPool would create Daemon threads, which are low priority threads. Threads such as created on the newSingleThreadExecutor, on the other hand, are high priority. So even if the main process is stopped, the high priority threads would continue its execution. (and if you can't stop them later they become zombie threads) – aran Jan 07 '21 at 04:05
  • *JVM will wait for all active user threads to finish their execution before it shutdown itself. On the other hand, a daemon thread doesn't get that preference, JVM will exit and close the Java program even if there is a daemon thread running in the background.* -- yay? – aran Jan 07 '21 at 04:10
  • 2
    it seems you confuse a lot of notions here, when the reality is that this is simple. I would suggest you read what a daemon thread is (it isn't a _low priority thread_). Priority for threads is an entirely different thing. – Eugene Jan 07 '21 at 04:10
  • Okay, true. Not to confuse with the priority levels of threads. The term "low priority" for the context is *"even if you are running, you stop here once the main thread finishes"*. My bad for using that term when there's another specification for priority regarding threads – aran Jan 07 '21 at 04:12
  • Daemon threads are meant to **serve user threads** and are only needed while user threads are running-that's why once "their" user thread finishes, it also finishes itself, because there's nothing to support or serve anymore, becoming useless. The executor would create a new Thread, meaning *"this is not to support this other thread, but an independent process by itself"*. So even if the other one stopped, I must continue my job, since I was not created to support or help the main thread. – aran Jan 07 '21 at 04:16