2

When will CompletableFuture releases thread back to ThreadPool ? Is it after calling get() method or after the associated task is completed?

Ravi
  • 30,829
  • 42
  • 119
  • 173
sreenath
  • 489
  • 4
  • 7

1 Answers1

6

There is no relationship between a get call and a thread from a pool. There isn’t even a relationship between the future’s completion and a thread.

A CompletableFuture can be completed from anywhere, e.g. by calling complete on it. When you use one of the convenience methods to schedule a task to an executor that will eventually attempt to complete it, then the thread will be used up to that point, when the completion attempt is made, regardless of whether the future is already completed or not.

For example,

CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> "hello");

is semantically equivalent to

CompletableFuture<String> f = new CompletableFuture<>();

ForkJoinPool.commonPool().execute(() -> {
    try {
        f.complete("hello");
    } catch(Throwable t) {
        f.completeExceptionally(t);
    }
});

It should be obvious that neither, the thread pool nor the scheduled action care for whether someone invokes get() or join() on the future or not.

Even when you complete the future earlier, e.g. via complete("some other string") or via cancel(…), it has no effect on the ongoing computation, as there is no reference from the future to the job. As the documentation of cancel states:

Parameters:

mayInterruptIfRunning - this value has no effect in this implementation because interrupts are not used to control processing.

Given the explanation above, it should be clear why.

There is a difference when you create a dependency chain, e.g. via b = a.thenApply(function). The job which will evaluate the function will not get submitted before a completed. By the time a completed, the completion status of b will be rechecked before the evaluation of function starts. If b has been completed at that time, the evaluation might get skipped.

CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return "foo";
});
CompletableFuture<String> b = a.thenApplyAsync(string -> {
    System.out.println("starting to evaluate "+string);
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    System.out.println("finishing to evaluate "+string);
    return string.toUpperCase();
});
b.complete("faster");
System.out.println(b.join());
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

will just print

faster

But once the evaluation started, it can’t be stopped, so

CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return "foo";
});
CompletableFuture<String> b = a.thenApplyAsync(string -> {
    System.out.println("starting to evaluate "+string);
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    System.out.println("finishing to evaluate "+string);
    return string.toUpperCase();
});
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
b.complete("faster");
System.out.println(b.join());
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

will print

starting to evaluate foo
faster
finishing to evaluate foo

showing that even by the time you successfully retrieved the value from the earlier completed future, there might be a still running background computation that will attempt to complete the future. But subsequent completion attempts will just be ignored.

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765