Vejledning til færdiggørelse af fremtiden

1. Introduktion

Denne vejledning er en guide til funktionaliteten og brugssagerne til Fuldført klasse, der blev introduceret som en forbedring af Java 8 Concurrency API.

2. Asynkron beregning i Java

Asynkron beregning er vanskelig at begrunde. Normalt vil vi tænke på enhver beregning som en række trin, men i tilfælde af asynkron beregning, handlinger repræsenteret som tilbagekald har tendens til enten at være spredt over koden eller dybt indlejrede i hinanden. Ting bliver endnu værre, når vi skal håndtere fejl, der kan opstå under et af trinene.

Det Fremtid interface blev tilføjet i Java 5 for at tjene som et resultat af en asynkron beregning, men den havde ingen metoder til at kombinere disse beregninger eller håndtere mulige fejl.

Java 8 introducerede Fuldført klasse. Sammen med Fremtid interface, implementerede det også FuldførelseStage interface. Denne grænseflade definerer kontrakten for et asynkront beregningstrin, som vi kan kombinere med andre trin.

Fuldført er samtidig en byggesten og en ramme med ca. 50 forskellige metoder til at komponere, kombinere og udføre asynkrone beregningstrin og håndteringsfejl.

En sådan stor API kan være overvældende, men disse falder for det meste i flere klare og tydelige brugssager.

3. Brug Fuldført som en simpel Fremtid

Først og fremmest Fuldført klasse implementerer Fremtid interface, så vi kan brug det som en Fremtid implementering, men med yderligere færdiggørelseslogik.

For eksempel kan vi oprette en forekomst af denne klasse med en no-arg-konstruktør, der repræsenterer et fremtidigt resultat, udleverer det til forbrugerne og udfylder det på et eller andet tidspunkt i fremtiden ved hjælp af komplet metode. Forbrugerne kan bruge metode til at blokere den aktuelle tråd, indtil dette resultat tilvejebringes.

I eksemplet nedenfor har vi en metode, der skaber en Fuldført eksempel, derefter spinner nogle beregninger ud i en anden tråd og returnerer Fremtid med det samme.

Når beregningen er færdig, afslutter metoden Fremtid ved at give resultatet til komplet metode:

offentlig Fremtidige beregneAsync () kaster InterruptedException {CompletableFuture completeableFuture = new CompletableFuture (); Executors.newCachedThreadPool (). Indsend (() -> {Thread.sleep (500); completeableFuture.complete ("Hej"); returner null;}); returner fuldstændigFuture; }

For at afslutte beregningen bruger vi Eksekutor API. Denne metode til oprettelse og udfyldelse af en Fuldført kan bruges sammen med enhver samtidighedsmekanisme eller API, inklusive rå tråde.

Læg mærke til det det calcAsync metode returnerer a Fremtid eksempel.

Vi kalder simpelthen metoden, modtager Fremtid eksempel, og ring til metode på det, når vi er klar til at blokere for resultatet.

Bemærk også, at metode kaster nogle kontrollerede undtagelser, nemlig ExecutionException (indkapsler en undtagelse, der opstod under en beregning) og Afbrudt undtagelse (en undtagelse, der betyder, at en tråd, der udfører en metode, blev afbrudt):

Fremtidig completableFuture = calcAsync (); // ... String result = completeableFuture.get (); assertEquals ("Hej", resultat);

Hvis vi allerede kender resultatet af en beregning, vi kan bruge det statiske completeFuture metode med et argument, der repræsenterer et resultat af denne beregning. Derfor er den metode til Fremtid vil aldrig blokere og returnerer straks dette resultat i stedet:

Future completeableFuture = CompletableFuture.completedFuture ("Hej"); // ... String result = completeableFuture.get (); assertEquals ("Hej", resultat);

Som et alternativt scenarie vil vi måske annullere udførelsen af ​​en Fremtid.

4. Fuldført med indkapslet beregningslogik

Koden ovenfor giver os mulighed for at vælge enhver mekanisme til samtidig udførelse, men hvad hvis vi vil springe denne kedelplade over og blot udføre nogle koder asynkront?

Statiske metoder runAsync og supplyAsync tillad os at oprette en Fuldført eksempel ud af Kan køres og Leverandør funktionelle typer tilsvarende.

Begge Kan køres og Leverandør er funktionelle grænseflader, der tillader overføring af deres forekomster som lambda-udtryk takket være den nye Java 8-funktion.

Det Kan køres interface er den samme gamle interface, der bruges i tråde, og det tillader ikke at returnere en værdi.

Det Leverandør interface er en generisk funktionel grænseflade med en enkelt metode, der ikke har nogen argumenter og returnerer en værdi af en parametreret type.

Dette giver os mulighed for give en instans af Leverandør som et lambda-udtryk, der foretager beregningen og returnerer resultatet. Det er så simpelt som:

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

5. Behandling af resultater af asynkrone beregninger

Den mest generiske måde at behandle resultatet af en beregning på er at føre det til en funktion. Det derefterAnvend metode gør netop det; det accepterer en Fungere bruger den til at behandle resultatet og returnerer en Fremtid der har en værdi, der returneres af en funktion:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"); CompletableFuture future = completeFuture .thenApply (s -> s + "Verden"); assertEquals ("Hello World", future.get ());

Hvis vi ikke har brug for at returnere en værdi ned på Fremtid kæde, kan vi bruge en forekomst af Forbruger funktionel grænseflade. Dens enkelte metode tager en parameter og returnerer ugyldig.

Der er en metode til denne brugssag i Fuldført Det derefter accepterer metoden modtager en Forbruger og sender det resultatet af beregningen. Så finalen future.get () opkald returnerer en forekomst af Ugyldig type:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"); CompletableFuture future = completeFuture .thenAccept (s -> System.out.println ("Beregning returneret:" + s)); future.get ();

Endelig, hvis vi hverken har brug for værdien af ​​beregningen eller ønsker at returnere en værdi i slutningen af ​​kæden, så kan vi videregive en Kan køres lambda til derefter Kør metode. I det følgende eksempel udskriver vi blot en linje i konsollen efter at have ringet til future.get ():

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"); CompletableFuture future = completeFuture .thenRun (() -> System.out.println ("Beregning afsluttet.")); future.get ();

6. Kombination af futures

Den bedste del af Fuldført API er evne til at kombinere Fuldført forekomster i en kæde af beregningstrin.

Resultatet af denne sammenkædning er i sig selv a Fuldført der muliggør yderligere sammenkædning og kombination. Denne tilgang er allestedsnærværende på funktionelle sprog og kaldes ofte et monadisk designmønster.

I det følgende eksempel bruger vi derefterKomponer metode til at kæde to Fremtid sekventielt.

Bemærk, at denne metode tager en funktion, der returnerer a Fuldført eksempel. Argumentet for denne funktion er resultatet af det forrige beregningstrin. Dette giver os mulighed for at bruge denne værdi inden i den næste Fuldført'S lambda:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"). DerefterCompose (s -> CompletableFuture.supplyAsync (() -> s + "Verden")); assertEquals ("Hello World", completableFuture.get ());

Det derefter komponere metode sammen med derefterAnvend, implementere grundlæggende byggesten i det monadiske mønster. De er tæt knyttet til kort og flatMap metoder til Strøm og Valgfri klasser også tilgængelige i Java 8.

Begge metoder modtager en funktion og anvender den på beregningsresultatet, men derefterKomponer (flatMap) metode modtager en funktion, der returnerer et andet objekt af samme type. Denne funktionelle struktur gør det muligt at komponere forekomsterne af disse klasser som byggesten.

Hvis vi ønsker at udføre to uafhængige Fremtid og gøre noget med deres resultater, kan vi bruge derefterKombiner metode, der accepterer en Fremtid og en Fungere med to argumenter til at behandle begge resultater:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"). DerefterCombine (CompletableFuture.supplyAsync (() -> "Verden"), (s1, s2) -> s1 + s2)); assertEquals ("Hello World", completableFuture.get ());

En enklere sag er, når vi vil gøre noget med to Fremtid‘Resultater, men behøver ikke at sende nogen resulterende værdi ned a Fremtid kæde. Det derefterAcceptBåde metoden er der for at hjælpe:

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

7. Forskellen mellem derefterAnvend () og derefterKomponer ()

I vores tidligere afsnit har vi vist eksempler vedrørende derefterAnvend () og derefterKomponer (). Begge API'er hjælper med at kæde forskellige Fuldført opkald, men brugen af ​​disse 2 funktioner er forskellig.

7.1. derefterAnvend ()

Vi kan bruge denne metode til at arbejde med et resultat af det forrige opkald. Et centralt punkt at huske er imidlertid, at returtypen kombineres af alle opkald.

Så denne metode er nyttig, når vi vil transformere resultatet af en Fuldført opkald:

CompletableFuture finalResult = beregne (). Derefter Anvend (s-> s + 1);

7.2. derefterKomponer ()

Det derefterKomponer () metoden ligner derefterAnvend () ved at begge returnerer en ny færdiggørelsesfase. Imidlertid, derefterKomponer () bruger den foregående fase som argumentet. Det vil flade og returnere a Fremtid med resultatet direkte, snarere end en indlejret fremtid, som vi observerede i derefterAnvend ():

CompletableFuture computeAnother (Heltal i) {returner CompletableFuture.supplyAsync (() -> 10 + i); } CompletableFuture finalResult = compute (). DerefterCompose (dette :: computeAnother);

Så hvis ideen er at kæde Fuldført metoder, så er det bedre at bruge derefterKomponer ().

Bemærk også, at forskellen mellem disse to metoder er analog med forskellen mellem kort() og flatMap ().

8. Kører flere Fremtid parallelt

Når vi har brug for at udføre flere Fremtid parallelt vil vi normalt vente på, at de alle udfører og derefter behandle deres samlede resultater.

Det CompletableFuture.allOf statisk metode gør det muligt at vente på færdiggørelse af alle Fremtid leveres som en var-arg:

CompletableFuture future1 = CompletableFuture.supplyAsync (() -> "Hej"); CompletableFuture future2 = CompletableFuture.supplyAsync (() -> "Smuk"); CompletableFuture future3 = CompletableFuture.supplyAsync (() -> "Verden"); CompletableFuture combinedFuture = CompletableFuture.allOf (future1, future2, future3); // ... combinedFuture.get (); assertTrue (future1.isDone ()); assertTrue (future2.isDone ()); assertTrue (future3.isDone ());

Bemærk, at returtypen af CompletableFuture.allOf () er en Fuldført. Begrænsningen ved denne metode er, at den ikke returnerer de samlede resultater af alle Fremtid. I stedet skal vi manuelt få resultater fra Fremtid. Heldigvis, CompletableFuture.join () metode og Java 8 Streams API gør det enkelt:

Streng kombineret = Stream.of (future1, future2, future3) .map (CompletableFuture :: join) .collect (Collectors.joining ("")); assertEquals ("Hej smukke verden", kombineret);

Det CompletableFuture.join () metoden ligner metode, men den kaster en ukontrolleret undtagelse i tilfælde af Fremtid fuldfører ikke normalt. Dette gør det muligt at bruge det som en metodehenvisning i Stream.map () metode.

9. Håndteringsfejl

For fejlhåndtering i en kæde af asynkrone beregningstrin skal vi tilpasse smide / fange idiom på en lignende måde.

I stedet for at fange en undtagelse i en syntaktisk blok, er Fuldført klasse giver os mulighed for at håndtere det i en speciel håndtere metode. Denne metode modtager to parametre: et resultat af en beregning (hvis den blev afsluttet med succes) og den undtagelse, der blev kastet (hvis et eller andet beregningstrin ikke blev fuldført normalt).

I det følgende eksempel bruger vi håndtere metode til at give en standardværdi, når den asynkrone beregning af en hilsen blev afsluttet med en fejl, fordi der ikke blev givet noget navn:

String name = null; // ... CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> {if (name == null) {throw new RuntimeException ("Computation error!");} Return "Hello," + name;})}). håndtag ((s, t) -> s! = null? s: "Hej fremmed!"); assertEquals ("Hello, Stranger!", completeableFuture.get ());

Antag som et alternativ scenarie, at vi ønsker at udfylde manuelt Fremtid med en værdi, som i det første eksempel, men har også evnen til at fuldføre den med en undtagelse. Det komplet undtagelsesvis metoden er beregnet til netop det. Det completableFuture.get () metode i det følgende eksempel kaster en ExecutionException med en RuntimeException som årsag:

CompletableFuture completableFuture = ny CompletableFuture (); // ... completeableFuture.completeExceptionally (ny RuntimeException ("Beregning mislykkedes!"); // ... completableFuture.get (); // ExecutionException

I eksemplet ovenfor kunne vi have håndteret undtagelsen med håndtere metode asynkront, men med metode kan vi bruge den mere typiske tilgang til en synkron behandling af undtagelser.

10. Asynkroniseringsmetoder

De fleste metoder til den flydende API i Fuldført klasse har to ekstra varianter med Async postfix. Disse metoder er normalt beregnet til kører et tilsvarende trin i udførelse i en anden tråd.

Metoderne uden Async postfix kør næste udførelsesfase ved hjælp af en kaldetråd. I modsætning hertil er Async metode uden Eksekutor argument kører et trin ved hjælp af det fælles fork / join pool implementering af Eksekutor der er adgang til med ForkJoinPool.commonPool () metode. Endelig blev Async metode med en Eksekutor argument kører et trin ved hjælp af bestået Eksekutor.

Her er et modificeret eksempel, der behandler resultatet af en beregning med en Fungere eksempel. Den eneste synlige forskel er derefterApplyAsync metode, men under emhætten er anvendelsen af ​​en funktion pakket ind i en ForkJoinTask eksempel (for mere information om fork / join ramme, se artiklen “Vejledning til gaffel / tilslut ramme i Java”). Dette giver os mulighed for at parallelisere vores beregning endnu mere og bruge systemressourcer mere effektivt:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "Hej"); CompletableFuture future = completeFuture .thenApplyAsync (s -> s + "Verden"); assertEquals ("Hello World", future.get ());

11. JDK 9 Fuldført API

Java 9 forbedrer Fuldført API med følgende ændringer:

  • Nye fabriksmetoder tilføjet
  • Støtte til forsinkelser og timeouts
  • Forbedret understøttelse af underklassificering

og nye instans-API'er:

  • Executor defaultExecutor ()
  • CompletableFuture newIncompleteFuture ()
  • CompletableFuture kopi ()
  • CompletionStage minimalCompletionStage ()
  • Kompletabel Fremtid komplet Async (leverandør leverandør, eksekutor eksekutor)
  • Kompletabel Fremtid komplet Async (leverandør leverandør)
  • CompletableFuture ellerTimeout (lang timeout, TimeUnit enhed)
  • CompletableFuture completeOnTimeout (T-værdi, lang timeout, TimeUnit-enhed)

Vi har nu også et par statiske hjælpemetoder:

  • Eksekutør forsinket Eksekutør (lang forsinkelse, TimeUnit enhed, Eksekutor eksekutor)
  • Eksekutør forsinket Eksekutør (lang forsinkelse, TimeUnit-enhed)
  • CompletionStage completedStage (U-værdi)
  • CompletionStage failedStage (Throwable ex)
  • CompletableFuture failedFuture (Throwable ex)

Endelig, for at adressere timeout, har Java 9 introduceret yderligere to nye funktioner:

  • ellerTimeout ()
  • completeOnTimeout ()

Her er den detaljerede artikel til yderligere læsning: Java 9 CompletableFuture API-forbedringer.

12. Konklusion

I denne artikel har vi beskrevet metoderne og typiske brugssager for Fuldført klasse.

Kildekoden til artiklen er tilgængelig på GitHub.