When will CompletableFuture
releases thread back to ThreadPool
? Is it after calling get()
method or after the associated task is completed?
1 Answers
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.