java.util.concurrent.CompletableFuture中的异常传播

时间:2019-02-26 07:26:04

标签: java java.util.concurrent completable-future forkjoinpool

有两段代码。

在第一个中,我们从任务中创建CompletableFuture,该任务总是会引发一些异常。然后,我们将“异常”方法应用于此未来,然后应用“ theAccept”方法。我们不会将Accept方法返回的新的Future赋给任何变量。然后,在原始的将来调用“ join”。我们看到的是,已经调用了“ exceptionally”方法以及“ thenAccept”方法。我们看到它是因为它们在输出中打印了适当的行。但是该异常尚未通过“异常”方法抑制。在这种情况下,禁止异常并为我们提供一些默认值,这正是我们从“例外情况”中获得的期望。

在第二个片段中,我们几乎执行相同的操作,但是将新返回的future分配给变量并在其上调用“ join”。在这种情况下,可以按预期抑制异常。

从我的观点来看,第一部分的一致行为是要么不抑制异常,要么不异常调用“ thenAccept”,要么异常调用并抑制异常。

为什么我们之间要有东西?

第一段:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

        future.exceptionally(e -> {
                    System.out.println("Exceptionally");
                    return 42;
                })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

        future.join();
    }

    private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

第二个片段:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

        CompletableFuture<Void> voidCompletableFuture = future.exceptionally(e -> {
            System.out.println("Exceptionally");
            return 42;
        })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

        voidCompletableFuture.join();
    }

    private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

1 个答案:

答案 0 :(得分:3)

没有“抑制异常”之类的东西。调用exceptionally时,您正在创建一个新的Future,如果前一个阶段异常完成,则将以上一阶段的结果或函数评估的结果来完成。上一个阶段(即您将来调用exceptionally的将来)不会受到影响。

这适用于链接依赖函数或动作的 all 方法。这些方法中的每一个都创造了一个新的未来,它将如文献所述完成。它们都不会影响调用该方法的现有未来。

也许,通过以下示例,它会变得更加清晰:

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return "a string";
});

CompletableFuture<Integer> f2 = f1.thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    return s.length();
});

f2.thenAccept(i -> System.out.println("result of f2 = "+i));

String s = f1.join();
System.out.println("result of f1 = "+s);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

在这里,应该清楚,依赖阶段Integer的结果不能取代前提阶段String的结果。这简直就是两种不同的未来,结果各不相同。而且,由于在join()上调用f1会查询第一阶段的结果,因此它不依赖于f2,因此甚至不需要等待其完成。 (这也是代码等待所有后台活动结束的原因)。

exceptionally的用法没有什么不同。在非例外情​​况下,下一个阶段具有相同的类型甚至是相同的结果可能会令人困惑,但是这并没有改变存在两个不同阶段的事实。

static void report(String s, CompletableFuture<?> f) {
    f.whenComplete((i,t) -> {
        if(t != null) System.out.println(s+" completed exceptionally with "+t);
        else System.out.println(s+" completed with value "+i);
    });
}
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    throw new IllegalArgumentException("Error for testing");
});
CompletableFuture<Integer> f2 = f1.exceptionally(t -> 42);

report("f1", f1);
report("f2", f2);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

CompletableFuture链接方法似乎已经成为一种为单个未来提供某种方式的构建者的广泛思维,不幸的是,这是误导性的错误。另一个陷阱是以下错误:

CompletableFuture<?> f = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("initial stage");
    return "";
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("second stage");
    return s;
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("third stage");
    return s;
}).thenAccept(s -> {
    System.out.println("last stage");
});

f.cancel(true);
report("f", f);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

正如所解释的,每个链接方法都创建一个新阶段,因此保留对最后一个链接方法所返回的阶段(即最后一个阶段)的引用很适合获得最终结果。但是取消此阶段只会取消最后一个阶段,而不会取消任何先决条件阶段。另外,取消之后,最后一个阶段不再依赖于其他阶段,因为它已经通过取消完成,并且能够报告此异常结果,而其他现在不相关的阶段仍在后台进行评估。