在 supplyAsync 阻塞主线程后使用 thenAccept

Using thenAccept after supplyAsync blocks the main thread

我正在开发一个与其他网络应用程序通信的网络应用程序。有时,我的系统会向其他系统发送 HTTP 请求作为通知。由于他们的响应对我来说不是必需的,所以我使用 Java 8 CompletableFuture supplyAsync 发送请求并使用 thenAccept 打印他们的响应,这样我的主线程就不会被阻塞。然而,我发现 CompletableFuture 函数链每次大约需要 100 到 200 毫秒,这让我感到困惑,因为根据我的理解,thenAccept() 应该 运行 在与 supplyAsync() 的同一线程中。

我用下面的代码模拟了我的过程

public static void run() {
    long start = System.currentTimeMillis();
    log.info("run start -> " + new Timestamp(start));
    CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 42;
    }).thenAccept(res -> log.info("run result -> " + res + ", time -> " + new Timestamp(System.currentTimeMillis())));
    log.info("run duration ->" + (System.currentTimeMillis() - start));
}

public static void runAsync() {
    long start = System.currentTimeMillis();
    log.info("runAsync start -> " + new Timestamp(start));
    CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 42;
    }).thenAcceptAsync(res -> log.info("runAsync result -> " + res + ", time ->" + new Timestamp(System.currentTimeMillis())));
    log.info("runAsync duration ->" + (System.currentTimeMillis() - start));
}

public static void main(String[] args) throws InterruptedException {
    Test.run();
    Test.runAsync();
    Thread.sleep(1000);
}

运行() 方法使用 thenAccept() 和 supplyAsync() 而 运行Async() 使用 thenAcceptAsync()。我希望它们都只需要几毫秒。但是,实际输出是:

10:04:54.632 [main] INFO Test - run start -> 2017-12-08 10:04:54.622
10:04:54.824 [main] INFO Test - run duration ->202
10:04:54.824 [main] INFO Test - runAsync start -> 2017-12-08 10:04:54.824
10:04:54.826 [main] INFO Test - runAsync duration ->2
10:04:55.333 [ForkJoinPool.commonPool-worker-1] INFO Test - run result -> 42, time -> 2017-12-08 10:04:55.333
10:04:55.333 [ForkJoinPool.commonPool-worker-3] INFO Test - runAsync result -> 42, time ->2017-12-08 10:04:55.333

我们可以看到 运行() 耗时 202 毫秒,是 运行Async() 耗时 2 毫秒的 100 倍。

我不明白这 202 毫秒的开销从何而来,显然它不是 supplyAysnc() 中休眠 500 毫秒的 lambda 函数。

谁能解释为什么 运行() 方法会阻塞,我是否应该始终使用 thenAcceptAsync() 而不是 thenAccept()?

非常感谢。

200 毫秒是线程池和支持它的所有 classes 的启动时间。

如果你交换主语句中的语句就变得很明显 class:

public static void main(String[] args) throws InterruptedException {
    Test.runAsync();
    Test.run();
    Thread.sleep(1000);
}

现在 Test.runAsync(); 是需要 200 毫秒并且 Test.run(); 在 2 毫秒内完成的调用

…because from my understanding thenAccept() should run in the same thread with supplyAsync()'s

你的理解有误。

来自 the documentation of CompletableFuture:

  • Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture, or by any other caller of a completion method.

最明显的结果是,当 future 已经完成时,传递给 thenAccept() 的函数将直接在调用者的线程中求值,因为 future 不可能命令完成它的线程。事实上,CompletableFuture 与线程根本没有关联,因为 任何人 都可以在其上调用 complete,而不仅仅是执行 [= 的线程14=] 你传给了 supplyAsync。这也是cancel不支持中断的原因。未来不知道哪个线程可能会尝试完成它。

不那么明显的后果是即使是上述行为也不能保证。短语“or by any other caller of a completion method”并不局限于注册依赖操作的完成方法的调用者。它也可以是任何其他调用者在同一个 future 上注册一个依赖操作。因此,如果两个线程在同一个 future 上同时调用 thenApply,它们中的任何一个都可能最终评估两个函数,甚至更奇怪,每个线程最终都可能执行另一个线程的操作。规范不排除。

对于您在问题中提供的测试用例,您更有可能测量初始化开销,如中所述。但是对于框架只会初始化一次的 Web 应用程序中的实际问题,您可能会因错误理解 thenApply 的行为(或任何 非异步 一般的链接方法)。如果你想确保评估不会发生在调用者的线程中,你必须使用 thenApplyAsync.