Lets say I have a CompletableFuture which wraps a blocking call like querying a backend using JDBC. In this case, since I am not passing any executor service as a parameter to CompletableFuture.supplyAsync(), the actual blocking work of fetching the resources over backend should be done by a thread within the common Fork/Join pool. Isn't it bad practice to have threads from common FJpool do blocking calls? The advantage I have here is that my main thread isn't blocking, since I'm delegating blocking calls to be run asynchronously. Check abt JDBC calls being blocking here . If this inference is true, why do have the option of using default common FJpool with CompletableFuture?
CompletableFuture<List<String>> fetchUnicorns =
CompletableFuture.supplyAsync(() -> {
return unicornService.getUnicorns();
});
fetchUnicorns.thenAccept(/**Do something with the result*/);
The reason why you should not use blocking calls (in this way), is that the common pool parallelism has been configured to utilize the existing CPU cores, assuming non-blocking jobs. Blocked threads will reduce the parallelism for other task using the same pool.
But there is an official solution to this:
class BlockingGetUnicorns implements ForkJoinPool.ManagedBlocker {
List<String> unicorns;
public boolean block() {
unicorns = unicornService.getUnicorns();
return true;
}
public boolean isReleasable() { return false; }
}
CompletableFuture<List<String>> fetchUnicorns =
CompletableFuture.supplyAsync(() -> {
BlockingGetUnicorns getThem = new BlockingGetUnicorns();
try {
ForkJoinPool.managedBlock(getThem);
} catch (InterruptedException ex) {
throw new AssertionError();
}
return getThem.unicorns;
});
ForkJoinPool.ManagedBlocker
is the abstraction of a potentially blocking operation that allows the Fork/Join pool to create compensation threads when it recognizes that a worker thread is about to be blocked.
It should be obvious that it is much easier to use
CompletableFuture<List<String>> fetchUnicorns =
CompletableFuture.supplyAsync(() -> unicornService.getUnicorns(),
Executors.newSingleThreadExecutor());
here. In a production environment, you would keep a reference to the executor, reuse it and eventually call shutDown
on it. For a use case where the executor is not reused,
CompletableFuture<List<String>> fetchUnicorns =
CompletableFuture.supplyAsync(() -> unicornService.getUnicorns(),
r -> new Thread(r).start());
would suffice, as then, the thread will be disposed automatically after the job’s completion.
If this inference is true, why do have the option of using default common FJpool with CompletableFuture?
Because not all work is blocking.
You have the option to schedule your blocking work on a custom executor with CompletableFuture.supplyAsync(Supplier<U>, Executor)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With