Почему результат этого кода недетерминированный?

#java #multithreading #completable-future

#java #многопоточность #завершаемый-будущее

Вопрос:

Я бы ожидал, что следующий код всегда будет печатать «Ошибка», но иногда он печатает «внешний вход». Я хочу, чтобы «outer» завершил работу, а «inner» использовал результат «outer» для генерации своего собственного результата, который завершается неудачей, если в будущем произойдет сбой. Что я делаю не так?

         CompletableFuture<String> outer = CompletableFuture.supplyAsync(() -> "outer");
        CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "inner");
        inner.completeExceptionally(new IllegalArgumentException());
        CompletableFuture<String> both = outer.thenApply(s -> {
            try {
                String i = inner.get();
                return s   i;
            } catch (InterruptedException |ExecutionException e) {
                throw new IllegalStateException(e);
            }
        });

        try {
            String o = both.get();
            System.out.println(o);
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("Errored");
        }
  

Комментарии:

1. inner вообще не ссылается outer , так как же он может использовать outer результат? both относится к ним обоим, что кажется разумным. Это то, что вы имели в виду?

2. Но это так, он использует outer результат в thenAccept блоке, нет?

3. Это не in inner , это in both . Вот как inner инициализируется: CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "inner"); — это вообще не используется outer .

Ответ №1:

На самом деле это довольно тривиально, чтобы понять это. Вам нужно более внимательно присмотреться к этому:

 CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "inner");
  

в частности, тот факт, что вы говорите supplyAsync , что означает в другом потоке. В то время как в вашем основном потоке (или любом другом) вы делаете : inner.completeExceptionally(new IllegalArgumentException()); .

Какой поток должен выиграть? Тот, из supplyAsync которого вы выполняете, или тот, который вы запускаете inner.completeExceptionally ? Конечно, в документации completeExceptionally упоминается об этом… Это базовая «гонка», которая сначала достигает «завершения» (обычно или через исключение) inner .

Комментарии:

1. Я думал, что вызов inner.completeExceptionally перед вызовом inner.get гарантирует, что исключение будет сгенерировано.

2. @Johnny a Future не является потоком, для его запуска не требуется операция «терминал» ( get/join может быть). CompletableFuture Перед вызовом можно выполнить get , как подразумевается в get документации: «Ожидает, если необходимо , завершения этого будущего …»

Ответ №2:

Ключ к пониманию того, что происходит, находится в javadoc for completeExceptionally .

Если он еще не завершен, вызывает вызовы get() и связанные с ними методы для создания данного исключения.

В вашем примере вы создаете два фьючерса, которые будут завершены асинхронно, затем вы вызываете completeExceptionally , чтобы сообщить одному из фьючерсов выдать исключение, а не выдавать результат.

Исходя из того, что вы говорите, кажется, что иногда inner будущее уже завершено к моменту вашего вызова completeExceptionally . В этом случае completeExceptionally вызов не повлияет (согласно спецификации), а последующий inner.get() выдаст результат, который был (уже) вычислен.

Это условие гонки.