将默认的通用分叉/加入池与CompletableFuture一起使用来执行长阻塞调用是不是不好的做法?

假设我有一个ComppletableFuture,它包装了一个阻塞调用,就像使用JDBC查询后端一样。在这种情况下,由于我没有将任何执行器服务作为参数传递给ComppletableFuture.supplyAsync(),因此通过后端获取资源的实际阻塞工作应由公共Fork/Join池中的线程完成。让来自常见 FJpool 的线程执行阻塞调用不是不好的做法吗?我在这里的优势是我的主线程没有阻塞,因为我委托阻塞调用异步运行。检查 abt JDBC 调用正在阻塞 这里 。如果这个推论是正确的,为什么可以选择将默认的通用FJpool与ComppletableFuture一起使用?

CompletableFuture<List<String>> fetchUnicorns  = 
    CompletableFuture.supplyAsync(() -> {
        return unicornService.getUnicorns();
    });

fetchUnicorns.thenAccept(/**Do something with the result*/);

答案 1

不应使用阻塞调用(以这种方式)的原因是,公共池并行性已配置为利用现有的 CPU 内核(假设非阻塞作业)。阻塞的线程将降低使用同一池的其他任务的并行度。

但是有一个官方的解决方案:

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是潜在阻塞操作的抽象,该操作允许 Fork/Join 池在识别到工作线程即将被阻塞时创建补偿线程。

很明显,它更容易使用

CompletableFuture<List<String>> fetchUnicorns  = 
    CompletableFuture.supplyAsync(() -> unicornService.getUnicorns(),
                                  Executors.newSingleThreadExecutor());

这里。在生产环境中,您将保留对执行器的引用,重用它并最终调用它。对于不重用执行程序的用例,shutDown

CompletableFuture<List<String>> fetchUnicorns  = 
    CompletableFuture.supplyAsync(() -> unicornService.getUnicorns(),
                                  r -> new Thread(r).start());

就足够了,因为这样,线程将在作业完成后自动释放。


答案 2

如果这个推论是正确的,为什么可以选择将默认的通用FJpool与ComppletableFuture一起使用?

因为并非所有工作都是阻塞的。

您可以选择在自定义执行器上安排阻止工作CompletableFuture.supplyAsync(Supplier<U>, Executor)


推荐