Начиная с Java8 в библиотеке появился класс CompletableFuture — это средство для передачи информации между параллельными потоками исполнения. Говоря проще это блокирующая очередь, способная передать только одно ссылочное значение. В отличие от обычной очереди, передает также исключение, если оно возникло при вычислении передаваемого значения.
Класс содержит несколько десятков методов, в которых легко потеряться. Данная статья классифицирует эти методы по нескольким признакам, чтобы в них было легко ориентироваться.
Для разминки познакомимся с новыми интерфейсами из пакета java.util.Function, которые используются как типы параметров во многих методах.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// два параметра, возвращает результат BiFunction<T, U,R> { R apply(T t, U u); } // два параметра, не возвращает результат BiConsumer<T,U> { void accept(T t, U u) } // один параметр, возвращает результат Function<T, R> { R apply(T t); } // один параметр, не возвращает результат Consumer<T> { void accept(T t); } // Без параметров, возвращает результат Supplier<T> { T get(); } <span class="hljs-comment">// Без параметров, не возвращает результат</span> Runnable { <span class="hljs-function"><span class="hljs-keyword"> void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span></span>; } |
Эти интерфейсы являются функциональными, то есть, значения этого типа могут быть заданы как ссылками на объекты, так и ссылками на методы или лямбда-выражениями.
Как средство передачи данных, класс CompletableFuture имеет два суб-интерфейса — для записи и для чтения, которые в свою очередь делятся на непосредственные (синхронные) и опосредованные (асинхронные). Программно выделен только суб-интерфейс непосредственного чтения (java.util.concurrent.Future, существующий со времен java 5), но в целях классификации полезно мысленно выделять и остальные. Кроме этого разделения по суб-интерфейсам, я также буду стараться отделять базовые методы и методы, реализующие частные случаи.
Для краткости вместо “объект типа CompletableFuture” будем говорить “фьючерс”. «Данный фьючерс» означает фьючерс, к которому применятся описываемый метод.
содержание
1. Интерфейс непосредственной записи
Базовых методов, понятно, два — записать значение и записать исключение:
|
1 2 3 |
<span class="hljs-function"><span class="hljs-keyword">boolean</span> <span class="hljs-title">complete</span><span class="hljs-params">(T value)</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">completeExceptionally</span><span class="hljs-params">(Throwable ex)</span> </span> |
с очевидной семантикой.
Прочие методы:
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">boolean</span> <span class="hljs-title">cancel</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span> mayInterruptIfRunning)</span> </span> |
эквивалентен completeExceptionally(new CancellationException). Введен для совместимости с java.util.concurrent.Future.
|
1 2 |
<span class="hljs-keyword">static</span> <U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">completedFuture</span><span class="hljs-params">(U value)</span> </span> |
эквивалентен CompletableFuture res=new CompletableFuture(); res.complete(value).
|
1 2 3 |
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">obtrudeValue</span><span class="hljs-params">(T value)</span> <span class="hljs-keyword">void</span> <span class="hljs-title">obtrudeException</span><span class="hljs-params">(Throwable ex)</span> </span> |
Насильно перезаписывают хранящееся значение. Верный способ выстрелить себе в ногу.
2. Интерфейс непосредственного чтения
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">boolean</span> <span class="hljs-title">isDone</span><span class="hljs-params">()</span> </span> |
Проверяет, был ли уже записан результат в данный фьючерс.
|
1 2 |
<span class="hljs-function">T <span class="hljs-title">get</span><span class="hljs-params">()</span> </span> |
Ждет, если результат еще не записан, и возвращает значение. Если было записано исключение, бросает ExecutionException.
Прочие методы:
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">boolean</span> <span class="hljs-title">isCancelled</span><span class="hljs-params">()</span> </span> |
проверяет, было ли записано исключение с помощью метода cancel().
|
1 2 |
<span class="hljs-function">T <span class="hljs-title">join</span><span class="hljs-params">()</span> </span> |
То же, что get(), но бросает CompletionException.
|
1 2 |
<span class="hljs-function">T <span class="hljs-title">get</span><span class="hljs-params">(<span class="hljs-keyword">long</span> timeout, TimeUnit unit)</span> </span> |
get() с тайм-аутом.
|
1 2 |
<span class="hljs-function">T <span class="hljs-title">getNow</span><span class="hljs-params">(T valueIfAbsent)</span> </span> |
возвращает результат немедленно. Если результат еще не записан, возвращает значение параметра valueIfAbsent.
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">getNumberOfDependents</span><span class="hljs-params">()</span> </span> |
примерное число других CompletableFuture, ждущих заполнения данного.
3. Интерфейс опосредованной записи
|
1 2 |
<span class="hljs-keyword">static</span> <U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">supplyAsync</span><span class="hljs-params">(Supplier<U> supplier)</span> </span> |
Запускается задача с функцией supplier, и результат выполнения записывается во фьючерс. Запуск задачи производится на стандартном пуле потоков.
|
1 2 |
<span class="hljs-keyword">static</span> <U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">supplyAsync</span><span class="hljs-params">(Supplier<U> supplier, Executor executor)</span> </span> |
То же самое, но запуск на пуле потоков, указанном параметром executor.
|
1 2 3 |
<span class="hljs-function"><span class="hljs-keyword">static</span> CompletableFuture<Void> <span class="hljs-title">runAsync</span><span class="hljs-params">(Runnable runnable)</span> <span class="hljs-keyword">static</span> CompletableFuture<Void> <span class="hljs-title">runAsync</span><span class="hljs-params">(Runnable runnable, Executor executor)</span> </span> |
То же самое, что и supplyAsync, но акция типа Runnable и, соответственно, результат будет типа Void.
4. Интерфейс опосредованного чтения
Предписывает выполнить заданное действие (реакцию) немедленно по заполнению этого (и/или другого) фьючерса. Самый обширный суб-интерфейс. Классифицируем его составляющие по двум признакам:
а) способ запуска реакции на заполнение: возможно запустить ее синхронно как метод при заполнении фьючерса, или асинхронно как задачу на пуле потоков. В случае асинхронного запуска используются методы с суффиксом Async (в двух вариантах — запуск на общем потоке ForkJoinPool.commonPool(), либо на потоке, указанном дополнительным параметром). Далее будут описываться только методы для синхронного запуска.
б) топология зависимости между данным фьючерсом и реакцией на его заполнение: линейная, типа “any“ и типа ”all”.
— линейная зависимость: один фьючерс поставляет одно значение в реакцию
— способ “any” — на входе два или более фьючерса; первый (по времени) результат, появившийся в одном из фьючерсов, передается в реакцию; остальные результаты игнорируются
— способ “all” — на входе два или более фьючерса; результаты всех фьючерсов накапливаются и затем передаются в реакцию.
4.1 Выполнить реакцию по заполнению данного фьючерса (линейная зависимость)
Эти методы имеют имена, начинающиеся с префикса then, имеют один параметр — реакцию, и возвращают новый фьючерс типа CompletableFuture для доступа к результату исполнения реакции. Различаются по типу реакции.
|
1 2 |
<U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">thenApply</span><span class="hljs-params">(Function<? <span class="hljs-keyword">super</span> T,? extends U> fn)</span> </span> |
Основной метод, в котором реакция получает значение из данного фьючерса и возвращаемое значения передается в результирующий фьючерс.
|
1 2 |
<span class="hljs-function">CompletableFuture<Void> <span class="hljs-title">thenAccept</span><span class="hljs-params">(Consumer<? <span class="hljs-keyword">super</span> T> block)</span> </span> |
Реакция получает значение из данного фьючерса, но не возвращает значения, так что
значение результирующего фьючерса имеет тип Void.
|
1 2 |
<span class="hljs-function">CompletableFuture<Void> <span class="hljs-title">thenRun</span><span class="hljs-params">(Runnable action)</span> </span> |
Реакция не получает и не возвращает значение.
Пусть compute1..compute4 — это ссылки на методы. Линейная цепочка с передачей значений от шага к шагу может выглядит так:
|
1 2 3 4 5 |
supplyAsync(compute1) .thenApply(compute2) .thenApply(compute3) .thenAccept(compute4); |
что эквивалентно простому вызову
|
1 2 |
compute4(compute3(compute2(compute1()))); |
|
1 2 |
<U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">thenCompose</span><span class="hljs-params">(Function<? <span class="hljs-keyword">super</span> T, CompletableFuture<U>> fn)</span> </span> |
То же, что thenApply, но реакция сама возвращает фьючерс вместо готового значения. Это может понадобиться, если нужно использовать реакцию сложной топологии.
4.2 Выполнить реакцию по заполнению любого из многих фьючерсов
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">static</span> CompletableFuture<Object> <span class="hljs-title">anyOf</span><span class="hljs-params">(CompletableFuture<?>... cfs)</span> </span> |
Возвращает новый фьючерс, который заполняется когда заполняется любой из фьючерсов, переданных параметром cfs. Результат совпадает с результатом завершившегося фьючерса.
4.3 Выполнить реакцию по заполнению любого из двух фьючерсов
Основной метод:
|
1 2 |
<U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">applyToEither</span><span class="hljs-params">(CompletableFuture<? extends T> other, Function<? <span class="hljs-keyword">super</span> T,U> fn)</span> </span> |
Возвращает новый фьючерс, который заполняется когда заполняется данный фьючерс либо фьючерс, переданный параметром other. Результат совпадает с результатом завершившегося фьючерса.
Метод эквивалентен выражению:
|
1 2 |
CompletableFuture.anyOf(<span class="hljs-keyword">this</span>, other).thenApply(fn); |
Остальные два метода отличаются лишь типом реакции:
|
1 2 3 |
<span class="hljs-function">CompletableFuture<Void> <span class="hljs-title">acceptEither</span><span class="hljs-params">(CompletableFuture<? extends T> other, Consumer<? <span class="hljs-keyword">super</span> T> block)</span> CompletableFuture<Void> <span class="hljs-title">runAfterEither</span><span class="hljs-params">(CompletableFuture<?> other, Runnable action)</span> </span> |
Непонятно, зачем было делать 3 метода *Either (9 с учетом *Async вариантов), когда достаточно было бы одного:
|
1 2 3 |
<T> <span class="hljs-function">CompletableFuture<T> <span class="hljs-title">either</span><span class="hljs-params">(CompletableFuture<? extends T> other)</span> </span>{ <span class="hljs-keyword">return</span> CompletableFuture.anyOf(<span class="hljs-keyword">this</span>, other); } |
тогда все эти методы можно было бы выразить как:
|
1 2 3 4 5 6 |
f1.applyToEither(other, fn) == f1.either(other).thenApply(fn); f1.applyToEitherAsync(other, fn) == f1.either(other).thenApplyAsync(fn); f1.applyToEitherAsync(other, fn, executor) == f1.either(other).thenApplyAsync(fn, executor); f1.acceptEither(other, block) == f1.either(other).thenAccept(other); f1.runAfterEither(other, action) == f1.either(other).thenRun(action); |
и т.п. Кроме того, метод either можно было бы использовать и в других комбинациях.
4.4 Выполнить реакцию по заполнению двух фьючерсов
|
1 2 |
<U,V> <span class="hljs-function">CompletableFuture<V> <span class="hljs-title">thenCombine</span><span class="hljs-params">(CompletionStage<? extends U> other, BiFunction<? <span class="hljs-keyword">super</span> T,? <span class="hljs-keyword">super</span> U,? extends V> fn)</span> </span> |
Основной метод. Имеет на входе два фьючерса, результаты которых накапливаются и затем передаются в реакцию, являющейся функцией от двух параметров.
Прочие методы отличаются типом реакции:
|
1 2 |
<U> <span class="hljs-function">CompletableFuture<Void> <span class="hljs-title">thenAcceptBoth</span><span class="hljs-params">(CompletableFuture<? extends U> other, BiConsumer<? <span class="hljs-keyword">super</span> T,? <span class="hljs-keyword">super</span> U> block)</span> </span> |
реакция не возвращает значение
|
1 2 |
<span class="hljs-function">CompletableFuture<Void> <span class="hljs-title">runAfterBoth</span><span class="hljs-params">(CompletableFuture<?> other, Runnable action)</span> </span> |
реакция не принимает параметров и не возвращает значение
4.5 Выполнить реакцию по заполнению многих фьючерсов
|
1 2 |
<span class="hljs-function"><span class="hljs-keyword">static</span> CompletableFuture<Void> <span class="hljs-title">allOf</span><span class="hljs-params">(CompletableFuture<?>... cfs)</span> </span> |
Возвращает CompletableFuture, завершающееся по завершению всех фьючерсов в списке параметров. Очевидный недостаток этого метода — в результирующий фьючерс не передаются значения, полученные во фьючерсах-параметрах, так что если они нужны, их нужно передавать каким-то другим способом.
4.6. Перехват ошибок исполнения
Если на каком-то этапе фьючерс завершается аварийно, исключение передается дальше по цепочке фьючерсов. Чтобы среагировать на ошибку и вернуться к нормальному исполнению, можно воспользоваться методами перехвата исключений.
|
1 2 |
<span class="hljs-function">CompletableFuture<T> <span class="hljs-title">exceptionally</span><span class="hljs-params">(Function<Throwable,? extends T> fn)</span> </span> |
Если данный фьючерс завершился аварийно, то результирующий фьючерс завершится с результатом, выработанным функцией fn. Если данный фьючерс завершился нормально, то результирующий фьючерс завершится нормально с тем же результатом.
|
1 2 |
<U> <span class="hljs-function">CompletableFuture<U> <span class="hljs-title">handle</span><span class="hljs-params">(BiFunction<? <span class="hljs-keyword">super</span> T,Throwable,? extends U> fn)</span> </span> |
В этом методе реакция вызывается всегда, независимо от того, заваершился ли данный фьючерс нормально или аварийно. Если фьючерс завершился нормально с результатом r, то в реакцию будут переданы параметры (r, null), если аварийно с исключением ex, то в реакцию будут переданы параметры (null, ex). Результат реакции может быть другого типа, нежели результат данного фьючерса.
Следующий пример взят из http://nurkiewicz.blogspot.ru/2013/05/java-8-definitive-guide-to.html:
|
1 2 3 4 5 6 7 8 9 |
CompletableFuture<Integer> safe = future.handle((r, ex) -> { <span class="hljs-keyword">if</span> (r != <span class="hljs-keyword">null</span>) { <span class="hljs-keyword">return</span> Integer.parseInt(r); } <span class="hljs-keyword">else</span> { log.warn(<span class="hljs-string">"Problem"</span>, ex); <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span>; } }); |
Здесь future вырабатывает результат типа String либо ошибку, реакция переводит результат в целое число, а в случае ошибки выдает -1. Заметим, что вообще-то проверку надо начинать с if (ex!=null), так как r==null может быть как при аварийном, так и нормальном завершении, но в данном примере случай r==null рассматривается как ошибка.
Заметка взята у Алексей Кайгородов @rfq: https://habrahabr.ru/post/213319/