Exception propagation in java.util.concurrent.Comp

2019-05-18 10:34发布

There are two snippets of code.

In the first one we create the CompletableFuture from the task which always throws some exception. Then we apply "exceptionally" method to this future, then "theAccept" method. We DO NOT assign new future returned by theAccept method to any variable. Then we invoke "join" on original future. What we see is that "exceptionally" method has been invoked as well as the "thenAccept". We see It because they printed appropriate lines in output. But the Exception has not been suppressed by "exceptionally" method. Suppress exception and provide us with some default value instead is exactly what we expected from "exceptionally" in this case.

In second snippet we do almost the same but assign new returned future to variable and invoke "join" on It. In this case as expected exception is suppressed.

From my point of view for the first part the consistent behavior is either not to suppress exception and not to invoke "exceptionally" and "thenAccept" or call exceptionally and suppress exception.

Why do we have something in between?

First snippet:

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

Second snippet:

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条回答
再贱就再见
2楼-- · 2019-05-18 11:30

There is no such thing as “suppressing an exception”. When you invoke exceptionally, you are creating a new future, which will be completed with the result of the previous stage or the result of evaluating the function if the previous stage completed exceptionally. The previous stage, i.e. the future you’re invoking exceptionally on, is not affected.

This applies to all methods chaining a depend function or action. Each of these methods creates a new future, which will be completed as documented. None of them affects the existing future you’re invoking the method on.

Perhaps, it becomes much clearer with the following example:

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

Here, it should be clear that the result of the dependent stage, an Integer, can’t supersede the result of the prerequisite stage, a String. These simply are two different futures with different results. And since calling join() on f1 queries for the result of the first stage, it isn’t dependent on f2 and hence, does not even wait for its completion. (That’s also the reason why the code waits for the end of all background activity at the end).

The usage of exceptionally is not different. It might be confusing that the next stage has the same type and even the same result in the non-exceptional case, but it doesn’t change the fact that there are two distinct stages.

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

There seems to be a widespread mindset of the CompletableFuture chaining methods to be some kind of builder for a single future, which unfortunately is misleadingly wrong. Another pitfall is the following mistake:

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

As explained, each chained method creates a new stage, so keeping a reference to the stage returned by the last chained method, i.e. the last stage, is suitable to get the final result. But canceling this stage will only cancel that last stage and none of the prerequisite stages. Also, after cancellation, the last stage does not depend on the other stages anymore, as it is already completed by cancellation and capable of reporting this exceptional result while the other, now unrelated stages are still being evaluated in the background.

查看更多
登录 后发表回答