首页 文章

在supplyAsync阻塞主线程之后使用thenAccept

提问于
浏览
1

我正在开发一个与其他Web应用程序通信的Web应用程序 . 我的系统有时会将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);
}

run()方法使用thenAccept()和supplyAsync(),而runAsync()使用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

我们可以看到run()需要202 ms,这是runAsync()持续时间的100倍,只使用2 ms .

我不明白202 ms开销来自哪里,显然它不是在supplyAysnc()中的lambda函数,它休眠500毫秒 .

任何人都可以解释为什么run()方法阻塞,我应该总是使用thenAcceptAsync()over thenAccept()吗?

非常感谢 .

2 回答

  • 0

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

    如果你在主类中交换语句就很明显了:

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

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

  • 0

    ...因为根据我的理解,thenAccept()应该与supplyAsync()在同一个线程中运行

    你的理解是错误的 .

    the documentation of CompletableFuture

    为非异步方法的依赖完成提供的动作可以由完成当前CompletableFuture的线程执行,也可以由完成方法的任何其他调用者执行 .

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

    不那么明显的后果是,即使上述行为也无法保证 . 短语“或由完成方法的任何其他调用者”不限制其注册依赖操作的完成方法的调用者 . 它也可以是任何其他呼叫者在同一个未来注册依赖行为 . 因此,如果两个线程在同一个未来同时调用 thenApply ,它们中的任何一个都可能最终评估这两个函数甚至更奇怪,每个线程最终可能会执行另一个线程的操作 . 规范并不排除它 .

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

相关问题