Sprievodca po CompletableFuture

1. Úvod

Tento návod je sprievodcom funkciami a prípadmi použitia CompletableFuture triedy, ktorá bola predstavená ako vylepšenie Java 8 Concurrency API.

2. Asynchrónny výpočet v Jave

Asynchrónny výpočet je ťažko odôvodniteľný. Zvyčajne chceme akýkoľvek výpočet považovať za sériu krokov, ale v prípade asynchrónneho výpočtu akcie reprezentované ako spätné volania majú tendenciu byť buď rozptýlené po celom kóde, alebo hlboko vnorené do seba. Situácia sa ešte zhoršuje, keď potrebujeme vyriešiť chyby, ktoré by sa mohli vyskytnúť počas jedného z krokov.

The Budúcnosť Do jazyka Java 5 bolo pridané rozhranie, ktoré slúžilo ako výsledok asynchrónneho výpočtu, ale nemalo žiadne metódy na kombinovanie týchto výpočtov alebo na zvládnutie možných chýb.

Java 8 predstavila CompletableFuture trieda. Spolu s Budúcnosť rozhranie, implementovala tiež DokončenieStage rozhranie. Toto rozhranie definuje kontrakt pre krok asynchrónneho výpočtu, ktorý môžeme kombinovať s ďalšími krokmi.

CompletableFuture je súčasne stavebným kameňom a rámcom s asi 50 rôznych metód na zostavovanie, kombinovanie a vykonávanie asynchrónnych výpočtových krokov a na spracovanie chýb.

Takéto veľké API môže byť ohromujúce, ale väčšinou spadá do niekoľkých jasných a zreteľných prípadov použitia.

3. Používanie CompletableFuture ako jednoduchý Budúcnosť

Najskôr zo všetkého CompletableFuture trieda implementuje Budúcnosť rozhranie, tak môžeme použite ako Budúcnosť implementácia, ale s ďalšou logikou dokončenia.

Napríklad môžeme vytvoriť inštanciu tejto triedy s konštruktorom no-arg, ktorý predstavuje nejaký budúci výsledok, odovzdať ho spotrebiteľom a dokončiť ho niekedy v budúcnosti pomocou kompletný metóda. Spotrebitelia môžu používať dostať metóda blokovania aktuálneho vlákna, kým sa nedosiahne tento výsledok.

V príklade nižšie máme metódu, ktorá vytvára a CompletableFuture inštancia, potom odtočí nejaký výpočet v inom vlákne a vráti Budúcnosť okamžite.

Keď je výpočet hotový, metóda dokončí Budúcnosť poskytnutím výsledku kompletný metóda:

public Future countAsync () hodí InterruptedException {CompletableFuture completableFuture = new CompletableFuture (); Executors.newCachedThreadPool (). Submit (() -> {Thread.sleep (500); completableFuture.complete ("Hello"); return null;}); return completableFuture; }

Na vypnutie výpočtu použijeme Exekútor API. Táto metóda vytvárania a dokončovania a CompletableFuture možno použiť spolu s akýmkoľvek mechanizmom súbežnosti alebo API, vrátane surových vlákien.

Všimni si the CalcAsync metóda vracia a Budúcnosť inštancia.

Jednoducho zavoláme metódu a dostaneme Budúcnosť napríklad a zavolajte na dostať keď sme pripravení zablokovať výsledok.

Upozorňujeme tiež, že dostať metóda vyvolá niektoré skontrolované výnimky, menovite ExecutionException (zapuzdrenie výnimky, ku ktorej došlo počas výpočtu) a Prerušená výnimka (výnimka znamenajúca, že vlákno vykonávajúce metódu bolo prerušené):

Future completableFuture = CalcAsync (); // ... Výsledok reťazca = completableFuture.get (); assertEquals ("Ahoj", výsledok);

Ak už vieme výsledok výpočtu, môžeme použiť statický dokončenáBudúcnosť metóda s argumentom, ktorý predstavuje výsledok tohto výpočtu. V dôsledku toho dostať metóda Budúcnosť nebude nikdy blokované, namiesto toho okamžite vráti tento výsledok:

Future completableFuture = CompletableFuture.completedFuture ("Dobrý deň"); // ... Výsledok reťazca = completableFuture.get (); assertEquals ("Ahoj", výsledok);

Ako alternatívny scenár by sme možno chceli zrušiť výkon a Budúcnosť.

4. CompletableFuture so zapuzdrenou výpočtovou logikou

Vyššie uvedený kód nám umožňuje zvoliť akýkoľvek mechanizmus súbežného vykonávania, ale čo keď chceme preskočiť tento základný program a jednoducho vykonať nejaký kód asynchrónne?

Statické metódy runAsync a supplyAsync dovoľte nám vytvoriť CompletableFuture inštancia z Spustiteľné a Dodávateľ funkčné typy zodpovedajúcim spôsobom.

Oboje Spustiteľné a Dodávateľ sú funkčné rozhrania, ktoré umožňujú posielať svoje inštancie ako výrazy lambda vďaka novej funkcii Java 8.

The Spustiteľné interface je to isté staré rozhranie, ktoré sa používa v vláknach a neumožňuje vrátiť hodnotu.

The Dodávateľ interface je všeobecné funkčné rozhranie s jedinou metódou, ktoré nemá žiadne argumenty a vracia hodnotu parametrizovaného typu.

To nám umožňuje poskytnúť inštanciu Dodávateľ ako výraz lambda, ktorý vykoná výpočet a vráti výsledok. Je to také jednoduché ako:

CompletableFuture future = CompletableFuture.supplyAsync (() -> "Hello"); // ... assertEquals ("Hello", future.get ());

5. Spracovanie výsledkov asynchrónnych výpočtov

Najobecnejším spôsobom, ako spracovať výsledok výpočtu, je dodať ho funkcii. The potomPoužiť metóda to robí presne; prijíma a Funkcia inštancia, použije ju na spracovanie výsledku a vráti a Budúcnosť ktorá obsahuje hodnotu vrátenú funkciou:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Hello"); CompletableFuture future = completableFuture .thenApply (s -> s + "svet"); assertEquals ("Hello World", future.get ());

Ak nepotrebujeme vrátiť hodnotu nadol Budúcnosť reťazca, môžeme použiť inštanciu Spotrebiteľ funkčné rozhranie. Jeho jediná metóda vezme parameter a vráti sa neplatný.

V tomto prípade existuje metóda pre tento prípad použitia CompletableFuture. The potom Prijať metóda prijíma a Spotrebiteľ a odovzdá mu výsledok výpočtu. Potom finále future.get () volanie vráti inštanciu súboru Neplatný typ:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Hello"); CompletableFuture future = completableFuture .thenAccept (s -> System.out.println ("Vrátený výpočet:" + s)); future.get ();

Nakoniec, ak ani nepotrebujeme hodnotu výpočtu, ani nechceme vrátiť nejakú hodnotu na konci reťazca, potom môžeme odovzdať Spustiteľné lambda do thenRun metóda. V nasledujúcom príklade jednoducho vytlačíme riadok v konzole po zavolaní future.get ():

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Hello"); CompletableFuture future = completableFuture .thenRun (() -> System.out.println ("Výpočet dokončený.")); future.get ();

6. Kombinácia budúcnosti

Najlepšia časť CompletableFuture API je schopnosť kombinovať CompletableFuture inštancií v reťazci výpočtových krokov.

Výsledkom tohto reťazenia je sám o sebe a CompletableFuture čo umožňuje ďalšie reťazenie a kombinovanie. Tento prístup je vo funkčných jazykoch všadeprítomný a často sa označuje ako monadický návrhový vzor.

V nasledujúcom príklade používame potom zložiť metóda na reťazenie dvoch Budúcnosť postupne.

Všimnite si, že táto metóda preberá funkciu, ktorá vracia a CompletableFuture inštancia. Argument tejto funkcie je výsledkom predchádzajúceho výpočtového kroku. To nám umožňuje použiť túto hodnotu v ďalšom CompletableFuture„Lambda:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Hello") .thComCom (s -> CompletableFuture.supplyAsync (() -> s + "svet")); assertEquals ("Hello World", completableFuture.get ());

The potom zložiť metóda spolu s potom Použiť, implementovať základné stavebné prvky monadického vzoru. Úzko súvisia s mapa a flatMap metódy Prúd a Voliteľné triedy dostupné aj v prostredí Java 8.

Obe metódy dostanú funkciu a použijú ju na výsledok výpočtu, ale potom zložiť (flatMap) metóda prijme funkciu, ktorá vráti iný objekt rovnakého typu. Táto funkčná štruktúra umožňuje skladať inštancie týchto tried ako stavebné bloky.

Ak chceme vykonať dve nezávislé Budúcnosť a urobíme niečo s ich výsledkami, môžeme použiť potomSkombinujte metóda, ktorá akceptuje a Budúcnosť a a Funkcia s dvoma argumentmi na spracovanie obidvoch výsledkov:

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

Jednoduchší prípad je, keď chceme urobiť niečo s dvoma Budúcnosť„Výsledky, ale nemusíte odovzdávať žiadnu výslednú hodnotu nadol a Budúcnosť reťaz. The thenAcceptBoth metóda je tu na pomoc:

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

7. Rozdiel medzi thenApply () a thenCompose ()

V našich predchádzajúcich častiach sme si ukázali príklady týkajúce sa thenApply () a thenCompose (). Obidve rozhrania API pomáhajú navzájom sa odlišovať CompletableFuture hovory, ale použitie týchto 2 funkcií je odlišné.

7.1. thenApply ()

Túto metódu môžeme použiť na prácu s výsledkom predchádzajúceho hovoru. Je však potrebné pamätať na to, že spätný typ bude kombinovaný zo všetkých hovorov.

Táto metóda je teda užitočná, keď chceme transformovať výsledok a CompletableFuture hovor:

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

7.2. thenCompose ()

The thenCompose () metóda je podobná ako thenApply () v tom, že obaja vrátia novú etapu dokončenia. Avšak thenCompose () ako argument používa predchádzajúcu etapu. Vyrovná sa a vráti a Budúcnosť s výsledkom priamo, nie ako vnorená budúcnosť, ako sme to pozorovali v roku thenApply ():

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

Takže ak ide o reťaz CompletableFuture metód, potom je lepšie ich použiť thenCompose ().

Upozorňujeme tiež, že rozdiel medzi týmito dvoma metódami je analogický s rozdielom medzi týmito metódami mapa () a flatMap ().

8. Spustenie viacerých Budúcnosť paralelne

Keď potrebujeme vykonať viac Budúcnosť paralelne zvyčajne chceme počkať, kým všetci vykonajú, a potom spracovať ich kombinované výsledky.

The CompletableFuture.allOf statická metóda umožňuje čakať na dokončenie všetkých Budúcnosť poskytnuté ako var-arg:

CompletableFuture future1 = CompletableFuture.supplyAsync (() -> „Dobrý deň“); CompletableFuture future2 = CompletableFuture.supplyAsync (() -> "Nádherný"); CompletableFuture future3 = CompletableFuture.supplyAsync (() -> "svet"); CompletableFuture combineFuture = CompletableFuture.allOf (future1, future2, future3); // ... combinedFuture.get (); assertTrue (future1.isDone ()); assertTrue (future2.isDone ()); assertTrue (future3.isDone ());

Všimnite si, že návratový typ CompletableFuture.allOf () je a CompletableFuture. Obmedzením tejto metódy je, že nevráti kombinované výsledky všetkých Budúcnosť. Namiesto toho musíme výsledky získať ručne Budúcnosť. Našťastie CompletableFuture.join () metóda a rozhranie Java 8 Streams API zjednodušuje:

Kombinovaný reťazec = Stream.of (future1, future2, future3) .map (CompletableFuture :: join) .collect (Collectors.joining ("")); assertEquals ("Hello Beautiful World", kombinované);

The CompletableFuture.join () metóda je podobná dostať metóda, ale vyvolá nekontrolovanú výnimku v prípade, že Budúcnosť nedokončí sa normálne. To umožňuje použiť ho ako referenciu metódy v Stream.map () metóda.

9. Zaobchádzanie s chybami

Pre spracovanie chýb v reťazci asynchrónnych výpočtových krokov musíme prispôsobiť hodiť / chytiť idiom podobným spôsobom.

Namiesto toho, aby sa chytila ​​výnimka v syntaktickom bloku, CompletableFuture trieda nám umožňuje zvládnuť to v špeciáli zvládnuť metóda. Táto metóda prijíma dva parametre: výsledok výpočtu (ak bol úspešne dokončený) a vyvolaná výnimka (ak sa niektorý krok výpočtu nedokončil normálne).

V nasledujúcom príklade používame znak zvládnuť metóda na zabezpečenie predvolenej hodnoty, keď bol asynchrónny výpočet pozdravu ukončený chybou, pretože nebol zadaný žiadny názov:

Názov reťazca = null; // ... CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> {if (name == null) {throw new RuntimeException ("Computation error!");} Return "Hello," + name;})}). handle ((s, t) -> s! = null? s: „Ahoj, cudzinec!“); assertEquals ("Hello, Stranger!", completableFuture.get ());

Ako alternatívny scenár predpokladajme, že chceme ručne vyplniť Budúcnosť s hodnotou, ako v prvom príklade, ale majú tiež schopnosť ju dokončiť s výnimkou. The kompletne Výnimočne metóda je určená práve na to. The completableFuture.get () metóda v nasledujúcom príklade hodí an ExecutionException s RuntimeException ako príčina:

CompletableFuture completableFuture = nový CompletableFuture (); // ... completableFuture.completeExceptionally (new RuntimeException ("Calculation failed!")); // ... completableFuture.get (); // ExecutionException

Vo vyššie uvedenom príklade sme mohli výnimku spracovať s zvládnuť metóda asynchrónne, ale s dostať metódu môžeme použiť typickejší prístup synchrónneho spracovania výnimiek.

10. Asynchronné metódy

Väčšina metód plynulého API v CompletableFuture triedy majú dva ďalšie varianty s Async postfix. Tieto metódy sú zvyčajne určené pre spustenie zodpovedajúceho kroku vykonania v inom vlákne.

Metódy bez Async postfix spustí ďalšiu etapu vykonávania pomocou volajúceho vlákna. Naproti tomu Async metóda bez Exekútor argument beží krok s použitím spoločného vidlica / pripojiť sa spoločná implementácia Exekútor ku ktorému je prístup pomocou ForkJoinPool.commonPool () metóda. Nakoniec Async metóda s Exekútor argument spustí krok s použitím odovzdaného Exekútor.

Tu je upravený príklad, ktorý spracuje výsledok výpočtu s a Funkcia inštancia. Jediný viditeľný rozdiel je thenApplyAsync metóda, ale pod kapotou je aplikácia funkcie zabalená do a ForkJoinTask napríklad (pre viac informácií o vidlica / pripojiť sa nájdete v článku „Sprievodca po rozhraní Fork / Join Framework v Jave“). To nám umožňuje ešte viac paralelizovať naše výpočty a efektívnejšie využívať systémové prostriedky:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Hello"); CompletableFuture future = completableFuture .thenApplyAsync (s -> s + "svet"); assertEquals ("Hello World", future.get ());

11. JDK 9 CompletableFuture API

Java 9 vylepšuje CompletableFuture API s nasledujúcimi zmenami:

  • Boli pridané nové továrenské metódy
  • Podpora oneskorení a časových limitov
  • Vylepšená podpora pre podtriedy

a nové API inštancie:

  • Vykonávateľ defaultExecutor ()
  • CompletableFuture newIncompleteFuture ()
  • CompletableFuture copy ()
  • CompletionStage minimalCompletionStage ()
  • CompletableFuture completeAsync (dodávateľ dodávateľa, exekútor vykonávateľ)
  • CompletableFuture completeAsync (dodávateľ dodávateľa)
  • CompletableFuture aleboTimeout (dlhý časový limit, jednotka TimeUnit)
  • CompletableFuture completeOnTimeout (hodnota T, dlhý časový limit, jednotka TimeUnit)

Teraz tiež máme niekoľko metód statickej utility:

  • Exekútor oneskorený Exekútor (dlhé oneskorenie, jednotka TimeUnit, exekútor exekútor)
  • Exekútor oneskorený Exekútor (dlhé oneskorenie, jednotka TimeUnit)
  • CompletionStage completedStage (hodnota U)
  • CompletionStage failedStage (hoditeľný ex)
  • CompletableFuture failedFuture (hoditeľný ex)

Nakoniec, kvôli vyriešeniu časového limitu, Java 9 predstavila ďalšie dve nové funkcie:

  • orTimeout ()
  • completeOnTimeout ()

Tu je podrobný článok na ďalšie čítanie: Vylepšenia rozhrania Java 9 CompletableFuture API.

12. Záver

V tomto článku sme opísali metódy a typické prípady použitia CompletableFuture trieda.

Zdrojový kód článku je k dispozícii na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found