Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it bad practice to use default common fork/join pool with CompletableFuture for doing long blocking calls?

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*/);
like image 724
Chinmay Avatar asked Aug 19 '17 23:08

Chinmay


2 Answers

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.

like image 55
Holger Avatar answered Nov 11 '22 21:11

Holger


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)

like image 28
the8472 Avatar answered Nov 11 '22 21:11

the8472