java - 为什么异常完成前调用get()会等待异常执行?

标签 java asynchronous java-8 future completable-future

在回答 this question 时,我注意到 CompletableFuture 的一个奇怪行为:如果你有一个 CompletableFuture cf 并用 cf.exceptionally() 链接一个调用,调用 cf.get() 表现异常:

  • 如果您在异常完成之前调用它,它会在返回之前等待 exceptionally() block 的执行
  • 否则,它会立即失败,抛出预期的 ExecutionException

我是不是遗漏了什么或者这是一个错误?我在 Ubuntu 17.04 上使用 Oracle JDK 1.8.0_131。

下面的代码说明了这种现象:

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
        sleep(1000);
        throw new RuntimeException("First");
    }).thenApply(Function.identity());

    future.exceptionally(e -> {
        sleep(1000);
        logDuration(start, "Exceptionally");
        return null;
    });

    final CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
        try {
            future.get();
        } catch (Exception e) {
        } finally {
            logDuration(start, "A");
        }
    });

    final CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> {
        sleep(1100);
        try {
            future.get();
        } catch (Exception e) {
        } finally {
            logDuration(start, "B");
        }
    });

    try {
        future.join();
    } catch (Exception e) {
        logDuration(start, "Main");
    }

    futureA.join();
    futureB.join();
}

private static void sleep(final int millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

private static void logDuration(long start, String who) {
    System.out.println(who + " waited for " + (System.currentTimeMillis() - start) + "ms");
}

输出:

B waited for 1347ms
Exceptionally waited for 2230ms
Main waited for 2230ms
A waited for 2230ms

如您所见,futureB 在调用 get() 之前 hibernate 了一会儿,根本没有阻塞。但是,futureA 和主线程都等待 exceptionally() 完成。

请注意,如果您删除 .thenApply(Function.identity()),则不会发生此行为。

最佳答案

唤醒 hibernate 线程是一个依赖操作,必须像其他任何操作一样处理,并且没有优先级。另一方面,轮询 CompletableFuture 的线程在它已经完成时不会进入 hibernate 状态,不需要被唤醒,因此不需要与其他依赖操作竞争。

用下面的程序

public static void main(String[] args) {
    final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
        waitAndLog("Supplier", null, 1000);
        throw new RuntimeException("First");
    }).thenApply(Function.identity());
    long start = System.nanoTime();

    CompletableFuture.runAsync(() -> waitAndLog("A", future, 0));

    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));

    future.exceptionally(e -> {
        waitAndLog("Exceptionally", null, 1000);
        return null;
    });

    CompletableFuture.runAsync(() -> waitAndLog("B", future, 0));
    CompletableFuture.runAsync(() -> waitAndLog("C", future, 1100));

    waitAndLog("Main", future, 0);
    ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS);
}
private static void waitAndLog(String msg, CompletableFuture<?> primary, int sleep) {
    long nanoTime = System.nanoTime();
    Object result;
    try {
        if(sleep>0) Thread.sleep(sleep);
        result = primary!=null? primary.get(): null;
    } catch (InterruptedException|ExecutionException ex) {
        result = ex;
    }
    long millis=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-nanoTime);
    System.out.println(msg+" waited for "+millis+"ms"+(result!=null? ", got "+result: ""));
}

我明白了,

Supplier waited for 993ms
A waited for 993ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
C waited for 1108ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
Exceptionally waited for 998ms
Main waited for 1983ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
B waited for 1984ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First

在我的机器上,表明在这个特定的案例中,相关操作是按照它们被安排的顺序执行的,首先是A .请注意,我在安排 Exceptionally 之前插入了额外的等待时间,这将是下一个相关操作。由于 B 在后台线程中运行,因此它是否设法将自身安排在 Main 线程之前是不确定的。我们可以在执行命令之前插入另一个延迟。

由于 C 轮询一个已经完成的 future ,它可以立即进行,因此它的净等待时间接近于明确指定的 hibernate 时间。

必须强调的是,这只是特定场景的结果,具体取决于实现细节。没有保证依赖操作的执行顺序。您自己可能已经注意到,如果没有 .thenApply(Function.identity()) 步骤,实现会运行不同的代码路径,从而导致相关操作的执行顺序不同。

依赖关系形成一棵树,实现必须以有效的方式遍历它而不会有堆栈溢出的风险,因此它必须以某种方式将其展平,并且对依赖关系树形状的微小变化可能会影响结果顺序一种非直观的方式。

关于java - 为什么异常完成前调用get()会等待异常执行?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44593117/

相关文章:

java - 如何获取对象中字段的名称?

ios - 如何在 swift 3 中专门化通用高阶函数?

javascript - 等待 requestAnimationFrame 完成(通过回调)

java - 使用 Java Stream API 求减法总和

java - 源与 PTransform

Java EE7 websocket 初始化 - 在第一个 @OnOpen 之前实现逻辑

java - 如何获取电子邮件 ID、显示名称和电话号码

node.js - 我没有获得 Node.js 的库异步,我错在哪里?

java - 如何使用 Java8s lambdas 改进日志记录机制

Java8动态代理和默认方法