Руководство по CompletableFuture

Руководство по CompletableFuture

1. Вступление

Эта статья представляет собой руководство по функциональности и вариантам использования классаCompletableFuture, представленного как усовершенствование Java 8 Concurrency API.

Дальнейшее чтение:

Runnable против Вызываемый в Java

Узнайте разницу между интерфейсами Runnable и Callable в Java.

Read more

Руководство по java.util.concurrent.Future

Руководство по java.util.concurrent.Future с обзором нескольких его реализаций

Read more

2. Асинхронные вычисления в Java

Асинхронные вычисления трудно рассуждать. Обычно мы хотим думать о любом вычислении как о последовательности шагов. Но в случае асинхронного вычисленияactions represented as callbacks tend to be either scattered across the code or deeply nested inside each other. Ситуация становится еще хуже, когда нам нужно обрабатывать ошибки, которые могут возникнуть во время одного из шагов.

ИнтерфейсFuture был добавлен в Java 5, чтобы служить результатом асинхронных вычислений, но у него не было никаких методов для объединения этих вычислений или обработки возможных ошибок.

In Java 8, the CompletableFuture class was introduced. Наряду с интерфейсомFuture он также реализовал интерфейсCompletionStage. Этот интерфейс определяет контракт для шага асинхронного вычисления, который можно комбинировать с другими шагами.

CompletableFuture - это одновременно строительный блок и каркас сabout 50 different methods for composing, combining, executing asynchronous computation steps and handling errors.

Такой большой API может быть подавляющим, но в большинстве случаев он подпадает под несколько четких и четких случаев использования.

3. ИспользованиеCompletableFuture в качестве простогоFuture

Прежде всего, классCompletableFuture реализует интерфейсFuture, поэтому вы можетеuse it as a Future implementation, but with additional completion logic.

Например, вы можете создать экземпляр этого класса с конструктором без аргументов, чтобы представить какой-то будущий результат, передать его потребителям и завершить его когда-нибудь в будущем, используя методcomplete. Потребители могут использовать методget для блокировки текущего потока до тех пор, пока не будет предоставлен этот результат.

В приведенном ниже примере у нас есть метод, который создает экземплярCompletableFuture, затем выполняет некоторые вычисления в другом потоке и немедленно возвращаетFuture.

Когда вычисление завершено, метод завершаетFuture, предоставляя результат методуcomplete:

public Future calculateAsync() throws InterruptedException {
    CompletableFuture completableFuture
      = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

Чтобы выделить вычисления, мы используем APIExecutor, который описан в статье“Introduction to Thread Pools in Java”, но этот метод создания и завершенияCompletableFuture может использоваться вместе с любым механизмом параллелизма или API. включая сырые потоки.

Обратите внимание, чтоthe calculateAsync method returns a Future instance.

Мы просто вызываем метод, получаем экземплярFuture и вызываем для него методget, когда мы готовы заблокировать результат.

Также обратите внимание, что методget выдает некоторые проверенные исключения, а именноExecutionException (инкапсулирует исключение, возникшее во время вычисления) иInterruptedException (исключение, означающее, что поток, выполняющий метод, был прерван) :

Future completableFuture = calculateAsync();

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

If you already know the result of a computation, вы можете использовать статический методcompletedFuture с аргументом, представляющим результат этого вычисления. Тогда методget дляFuture никогда не будет блокироваться, вместо этого немедленно возвращая этот результат.

Future completableFuture =
  CompletableFuture.completedFuture("Hello");

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

В качестве альтернативного сценария вы можете захотетьcancel the execution of a Future.

Допустим, нам не удалось найти результат и мы решили полностью отменить асинхронное выполнение. Это можно сделать с помощью методаFuture‘scancel. Этот метод получает аргументbooleanmayInterruptIfRunning, но в случаеCompletableFuture он не действует, поскольку прерывания не используются для управления обработкойCompletableFuture.

Вот модифицированная версия асинхронного метода:

public Future calculateAsyncWithCancellation() throws InterruptedException {
    CompletableFuture completableFuture = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.cancel(false);
        return null;
    });

    return completableFuture;
}

Когда мы блокируем результат с помощью методаFuture.get(), он выбрасываетCancellationException, если будущее отменено:

Future future = calculateAsyncWithCancellation();
future.get(); // CancellationException

4. CompletableFuture с инкапсулированной вычислительной логикой

Приведенный выше код позволяет нам выбрать любой механизм параллельного выполнения, но что, если мы хотим пропустить этот шаблон и просто выполнить некоторый код асинхронно?

Статические методыrunAsync иsupplyAsync позволяют нам создать экземплярCompletableFuture из функциональных типовRunnable иSupplier соответственно.

ИRunnable, иSupplier - это функциональные интерфейсы, которые позволяют передавать свои экземпляры в виде лямбда-выражений благодаря новой функции Java 8.

ИнтерфейсRunnable - это тот же старый интерфейс, который используется в потоках, и он не позволяет возвращать значение.

ИнтерфейсSupplier - это общий функциональный интерфейс с одним методом, который не имеет аргументов и возвращает значение параметризованного типа.

Это позволяетprovide an instance of the Supplier as a lambda expression that does the calculation and returns the result. Это так просто, как:

CompletableFuture future
  = CompletableFuture.supplyAsync(() -> "Hello");

// ...

assertEquals("Hello", future.get());

5. Обработка результатов асинхронных вычислений

Самый общий способ обработки результата вычислений - передать его функции. МетодthenApply делает именно это: принимает экземплярFunction, использует его для обработки результата и возвращаетFuture, который содержит значение, возвращаемое функцией:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApply(s -> s + " World");

assertEquals("Hello World", future.get());

Если вам не нужно возвращать значение по цепочкеFuture, вы можете использовать экземпляр функционального интерфейсаConsumer. Его единственный метод принимает параметр и возвращаетvoid.

ВCompletableFuture есть метод для этого варианта использования - методthenAccept получаетConsumer и передает ему результат вычисления. Последний вызовfuture.get() возвращает экземпляр типаVoid.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenAccept(s -> System.out.println("Computation returned: " + s));

future.get();

Наконец, если вам не нужно значение вычисления и вы не хотите возвращать какое-либо значение в конце цепочки, вы можете передать лямбдаRunnable методуthenRun. В следующем примере после вызова методаfuture.get() мы просто печатаем строку в консоли:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenRun(() -> System.out.println("Computation finished."));

future.get();

6. Объединение фьючерсов

Лучшая часть APICompletableFuture - этоability to combine CompletableFuture instances in a chain of computation steps.

Результатом этой цепочки являетсяCompletableFuture, который позволяет выполнять дальнейшую цепочку и комбинирование. Этот подход повсеместен в функциональных языках и часто называется монадическим шаблоном проектирования.

В следующем примере мы используем методthenCompose для последовательного связывания двухFutures.

Обратите внимание, что этот метод принимает функцию, которая возвращает экземплярCompletableFuture. Аргумент этой функции является результатом предыдущего шага вычисления. Это позволяет нам использовать это значение в лямбде следующегоCompletableFuture:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));

assertEquals("Hello World", completableFuture.get());

МетодthenCompose вместе сthenApply реализуют основные строительные блоки монадического шаблона. Они тесно связаны с методамиmap иflatMap классовStream иOptional, также доступных в Java 8.

Оба метода получают функцию и применяют ее к результату вычисления, но методthenCompose (flatMap)receives a function that returns another object of the same type. Эта функциональная структура позволяет составлять экземпляры этих классов как строительные блоки.

Если вы хотите выполнить два независимыхFutures и что-то сделать с их результатами, используйте методthenCombine, который принимаетFuture иFunction с двумя аргументами для обработки обоих результатов:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(
      () -> " World"), (s1, s2) -> s1 + s2));

assertEquals("Hello World", completableFuture.get());

Более простой случай - это когда вы хотите что-то сделать с двумя результатамиFutures, но вам не нужно передавать какое-либо полученное значение по цепочкеFuture. МетодthenAcceptBoth поможет:

CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
  .thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
    (s1, s2) -> System.out.println(s1 + s2));

7. Разница междуthenApply() иthenCompose()

В наших предыдущих разделах мы показали примеры, касающиесяthenApply() иthenCompose(). Оба API помогают связывать разные вызовыCompletableFuture, но использование этих двух функций отличается.

7.1. thenApply()с

This method is used for working with a result of the previous call. Однако важно помнить, что возвращаемый тип будет объединен из всех вызовов.

Таким образом, этот метод полезен, когда мы хотим преобразовать результат scallCompletableFuture :

CompletableFuture finalResult = compute().thenApply(s-> s + 1);

7.2. thenCompose()с

МетодthenCompose() похож наthenApply() тем, что оба возвращают новую стадию завершения. ОднакоthenCompose() uses the previous stage as the argument. Он сгладит и вернетFuture с результатом напрямую, а не вложенное будущее, как мы наблюдали вthenApply():

CompletableFuture computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture finalResult = compute().thenCompose(this::computeAnother);

Так что, если идея состоит в том, чтобы связать методыCompletableFuture, лучше использоватьthenCompose().

Также обратите внимание, что разница между этими двумя методами аналогичнаhttps://www.example.com/java-difference-map-and-flatmap.

8. Параллельное выполнение несколькихFutures

Когда нам нужно выполнить несколькоFuturesпараллельно, мы обычно хотим дождаться их выполнения, а затем обработать их объединенные результаты.

Статический методCompletableFuture.allOf позволяет дождаться завершения всехFutures, предоставленных как var-arg:

CompletableFuture future1
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture future2
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture future3
  = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture combinedFuture
  = CompletableFuture.allOf(future1, future2, future3);

// ...

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

Обратите внимание, что тип возвратаCompletableFuture.allOf() - этоCompletableFuture<Void>. Ограничение этого метода заключается в том, что он не возвращает объединенные результаты всехFutures. Вместо этого вам нужно вручную получить результаты изFutures. К счастью, методCompletableFuture.join() и Java 8 Streams API упрощают задачу:

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));

assertEquals("Hello Beautiful World", combined);

МетодCompletableFuture.join() похож на методget, но выдает непроверенное исключение в случае, еслиFuture не завершается нормально. Это позволяет использовать его как ссылку на метод в методеStream.map().

9. Обработка ошибок

Для обработки ошибок в цепочке шагов асинхронных вычислений идиомуthrow/catch пришлось адаптировать аналогичным образом.

Вместо того, чтобы перехватывать исключение в синтаксическом блоке, классCompletableFuture позволяет обрабатывать его с помощью специального методаhandle. Этот метод получает два параметра: результат вычисления (если он успешно завершился) и генерируемое исключение (если какой-либо шаг вычисления не завершился нормально).

В следующем примере мы используем методhandle для предоставления значения по умолчанию, когда асинхронное вычисление приветствия завершилось с ошибкой, поскольку имя не было предоставлено:

String name = null;

// ...

CompletableFuture completableFuture
  =  CompletableFuture.supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  })}).handle((s, t) -> s != null ? s : "Hello, Stranger!");

assertEquals("Hello, Stranger!", completableFuture.get());

В качестве альтернативного сценария предположим, что мы хотим вручную заполнитьFuture значением, как в первом примере, но также иметь возможность заполнить его с исключением. Для этого предназначен методcompleteExceptionally. МетодcompletableFuture.get() в следующем примере выдаетExecutionException сRuntimeException в качестве причины:

CompletableFuture completableFuture = new CompletableFuture<>();

// ...

completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));

// ...

completableFuture.get(); // ExecutionException

В приведенном выше примере мы могли бы обработать исключение с помощью методаhandle асинхронно, но с методомget мы можем использовать более типичный подход синхронной обработки исключения.

10. Асинхронные методы

Большинство методов свободного API в классеCompletableFuture имеют два дополнительных варианта с постфиксомAsync. Эти методы обычно предназначены дляrunning a corresponding step of execution in another thread.

Методы без постфиксаAsync запускают следующий этап выполнения с использованием вызывающего потока. МетодAsync без аргументаExecutor выполняет шаг, используя общую реализацию пулаfork/joinExecutor, доступ к которой осуществляется с помощью методаForkJoinPool.commonPool(). МетодAsync с аргументомExecutor выполняет шаг, используя переданныйExecutor.

Вот модифицированный пример, который обрабатывает результат вычисления с экземпляромFunction. Единственное видимое отличие - это методthenApplyAsync. Но под капотом приложение функции заключено в экземплярForkJoinTask (дополнительную информацию о структуреfork/join см. В статье“Guide to the Fork/Join Framework in Java”). Это позволяет распараллелить ваши вычисления еще эффективнее и использовать системные ресурсы.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApplyAsync(s -> s + " World");

assertEquals("Hello World", future.get());

11. API JDK 9CompletableFuture

В Java 9 APICompletableFuture был дополнительно усовершенствован со следующими изменениями:

  • Добавлены новые фабричные методы

  • Поддержка задержек и тайм-аутов

  • Улучшена поддержка подклассов.

Новые API экземпляров были представлены:

  • Исполнитель defaultExecutor ()

  • CompletableFuture newIncompleteFuture ()

  • CompletableFuture копия ()

  • CompletionStage minimalCompletionStage ()

  • CompletableFuture completeAsync (Поставщик поставщик, исполнитель исполнитель)

  • CompletableFuture completeAsync (Поставщик поставщика)

  • CompletableFuture orTimeout (длинный таймаут, единица TimeUnit)

  • CompletableFuture completeOnTimeout (значение T, длительный тайм-аут, единица TimeUnit)

Теперь у нас есть несколько статических утилит:

  • Executor delayedExecutor (длинная задержка, блок TimeUnit, исполнитель-исполнитель)

  • Executor delayedExecutor (длинная задержка, единица TimeUnit)

  • CompletionStage completedStage (значение U)

  • CompletionStage failedStage (Throwable ex)

  • CompletableFuture failedFuture (Throwable ex)

Наконец, для устранения таймаута Java 9 представила еще две новые функции:

  • orTimeout ()

  • completeOnTimeout ()

Вот подробная статья для дальнейшего чтения:Java 9 CompletableFuture API Improvements.

12. Заключение

В этой статье мы описали методы и типичные варианты использования классаCompletableFuture.

Исходный код статьи доступенover on GitHub.