【问题标题】:Exception propagation in java.util.concurrent.CompletableFuturejava.util.concurrent.CompletableFuture 中的异常传播
【发布时间】:2019-02-26 07:26:04
【问题描述】:

有两个sn-ps代码。

在第一个中,我们从总是抛出一些异常的任务中创建 CompletableFuture。然后我们将“例外”方法应用于这个未来,然后是“接受”方法。我们不会将Accept 方法返回的新未来分配给任何变量。然后我们在原始未来调用“加入”。我们看到的是“异常”方法以及“thenAccept”已被调用。我们看到它是因为他们在输出中打印了适当的行。但是异常并没有被“异常”方法抑制。在这种情况下,抑制异常并为我们提供一些默认值正是我们对“异常”的期望。

在第二个 sn-p 中,我们做了几乎相同的操作,但将新返回的未来分配给变量并在其上调用“join”。在这种情况下,正如预期的那样,异常被抑制了。

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

为什么我们之间有一些东西?

第一个sn-p:

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");
    }
}

第二个sn-p:

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");
    }
}

【问题讨论】:

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


    【解决方案1】:

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

    这适用于链接依赖函数或操作的所有方法。这些方法中的每一种都创造了一个新的未来,它将按照记录的方式完成。它们都不会影响您正在调用该方法的现有未来。

    也许,下面的例子会变得更清楚:

    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。这只是两个不同的未来,不同的结果。并且由于在f1 上调用join() 查询第一阶段的结果,它不依赖于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);
    

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

    【讨论】:

    • 问题有点不同。我完全理解链接不是单一未来的一种构建器。但是正如您在第一个 sn-p 中看到的那样,我不会存储通过链接到任何变量返回的未来。我加入的未来不依赖于任何其他未来。那么为什么无论如何都会调用“异常”呢?
    • 链接到未来的动作将尝试完成新创建的未来,无论您是否存储对它的引用。该代码应该如何知道您是否存储了对未来的引用?
    • 新创造的未来确实取决于原来的未来而不是相反,对吧?又为什么原来的未来要努力去完成新创造的未来呢?对不起,也许这是个愚蠢的问题,但我想了解
    • 它这样做了,因为你告诉过它。在 future 上调用 exceptionally 就像注册一个监听器以完成它。未来不关心监听器做什么,它与thenRun 等没有什么不同。为了保持这个类比,你只能通过取消注册来选择退出监听器,这需要对监听器的引用。如果您不保留它,侦听器仍将运行,但无法取消注册。同样,当您不保留对新创建的未来的引用时,您无法取消它,因此您失去了选择退出的可能性。
    • 好吧,“异常”和任何其他此类方法不仅会创建并返回新的未来对象,还会为它们所应用的对象注册一个侦听器。但是如果我们已经存储了引用并且在它上有了联合,那么异常就不会被传播,我们会得到“异常”提供的存根,对吧。这是我想了解的。谢谢!!
    猜你喜欢
    • 2015-12-25
    • 1970-01-01
    • 2016-06-24
    • 2017-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-22
    • 2011-11-08
    相关资源
    最近更新 更多