Vejledning til java.util.concurrent.Future

1. Oversigt

I denne artikel vil vi lære om Fremtid. En grænseflade, der har eksisteret siden Java 1.5, og som kan være ret nyttig, når man arbejder med asynkrone opkald og samtidig behandling.

2. Oprettelse Fremtid

Kort sagt, den Fremtid klasse repræsenterer et fremtidigt resultat af en asynkron beregning - et resultat, der til sidst vises i Fremtid efter at behandlingen er afsluttet.

Lad os se, hvordan man skriver metoder, der opretter og returnerer a Fremtid eksempel.

Langvarige metoder er gode kandidater til asynkron behandling og Fremtid interface. Dette gør det muligt for os at udføre en anden proces, mens vi venter på den opgave, der er indkapslet i Fremtid at færdiggøre.

Nogle eksempler på operationer, der vil udnytte asynkroniseringen af Fremtid er:

  • beregningsintensive processer (matematiske og videnskabelige beregninger)
  • manipulering af store datastrukturer (big data)
  • fjernmetodeopkald (download af filer, HTML-skrotning, webservices).

2.1. Implementerer Fremtid Med FutureTask

For vores eksempel skal vi oprette en meget enkel klasse, der beregner kvadratet af en Heltal. Dette passer bestemt ikke til kategorien "langvarig" metode, men vi vil sætte en Thread.sleep () ring til det for at få det til at vare 1 sekund at fuldføre:

offentlig klasse SquareCalculator {private ExecutorService eksekutor = Executors.newSingleThreadExecutor (); offentlig Fremtidsberegning (Integer input) {return executor.submit (() -> {Thread.sleep (1000); return input * input;}); }}

Den bit kode, der rent faktisk udfører beregningen, er indeholdt i opkald() metode, leveret som et lambda-udtryk. Som du kan se, er der ikke noget særligt ved det, bortset fra søvn() opkald nævnt tidligere.

Det bliver mere interessant, når vi retter vores opmærksomhed mod brugen af Kan kaldes og ExecutorService.

Kan kaldes er en grænseflade, der repræsenterer en opgave, der returnerer et resultat og har en enkelt opkald() metode. Her har vi oprettet en forekomst af det ved hjælp af et lambda-udtryk.

Oprettelse af en forekomst af Kan kaldes tager os ikke med nogen steder, vi er stadig nødt til at videregive denne instans til en eksekutor, der sørger for at starte opgaven i en ny tråd og give os det værdifulde tilbage Fremtid objekt. Det er her ExecutorService kommer i.

Der er et par måder, vi kan få fat på ExecutorService F.eks. leveres de fleste af dem i utility class Eksekutører ' statiske fabriksmetoder. I dette eksempel har vi brugt det grundlæggende newSingleThreadExecutor (), hvilket giver os en ExecutorService i stand til at håndtere en enkelt tråd ad gangen.

Når vi har en ExecutorService objekt, vi skal bare ringe Indsend() passerer vores Kan kaldes som et argument. Indsend() tager sig af start af opgaven og returnerer a FutureTask objekt, som er en implementering af Fremtid interface.

3. Forbruger Fremtid

Indtil dette har vi lært, hvordan man opretter en forekomst af Fremtid.

I dette afsnit lærer vi at arbejde med denne forekomst ved at udforske alle metoder, der er en del af Fremtid'S API.

3.1. Ved brug af Er gjort() og få() for at opnå resultater

Nu skal vi ringe Beregn() og brug den returnerede Fremtid for at få det resulterende Heltal. To metoder fra Fremtid API hjælper os med denne opgave.

Future.isDone () fortæller os, om eksekutøren er færdig med at behandle opgaven. Hvis opgaven er afsluttet, vender den tilbage rigtigt ellers vender det tilbage falsk.

Den metode, der returnerer det faktiske resultat fra beregningen er Future.get (). Bemærk, at denne metode blokerer udførelsen, indtil opgaven er afsluttet, men i vores eksempel vil dette ikke være et problem, da vi først kontrollerer, om opgaven er afsluttet ved at ringe Er gjort().

Ved at bruge disse to metoder kan vi køre en anden kode, mens vi venter på, at hovedopgaven er færdig:

Fremtidens fremtid = ny SquareCalculator (). Beregne (10); mens (! future.isDone ()) {System.out.println ("Beregner ..."); Tråd. Søvn (300); } Heltalsresultat = future.get ();

I dette eksempel skriver vi en simpel besked på output for at lade brugeren vide, at programmet udfører beregningen.

Metoden få() vil blokere udførelsen, indtil opgaven er afsluttet. Men det behøver vi ikke bekymre os om, da vores eksempel kun kommer til det punkt, hvor få() kaldes efter at have sørget for, at opgaven er afsluttet. Så i dette scenarie, future.get () vender altid straks tilbage.

Det er værd at nævne det få() har en overbelastet version, der tager en timeout og en TimeUnit som argumenter:

Heltalsresultat = future.get (500, TimeUnit.MILLISECONDS);

Forskellen på få (lang, TimeUnit) og få(), er, at førstnævnte vil kaste et TimeoutException hvis opgaven ikke vender tilbage inden den angivne timeoutperiode.

3.2. Annullering af en Fremtidige Wmed afbestille()

Antag, at vi har udløst en opgave, men af ​​en eller anden grund er vi ligeglad med resultatet længere. Vi kan bruge Future.cancel (boolsk) at bede eksekutøren om at stoppe operationen og afbryde dens underliggende tråd:

Fremtidig fremtid = ny SquareCalculator (). Beregne (4); boolsk annulleret = future.cancel (true);

Vores instans af Fremtid fra ovenstående kode ville aldrig fuldføre dens drift. Faktisk, hvis vi prøver at ringe få() fra det tilfælde, efter opkaldet til afbestille(), ville resultatet være en Annullering Undtagelse. Future.isCancelled () vil fortælle os, om en Fremtid var allerede annulleret. Dette kan være ret nyttigt for at undgå at få en Annullering Undtagelse.

Det er muligt, at et opkald til afbestille() mislykkes. I så fald vil den returnerede værdi være falsk. Læg mærke til det afbestille() tager en boolsk værdi som argument - dette styrer, om tråden, der udfører denne opgave, skal afbrydes eller ej.

4. Mere multithreading med Tråd Puljer

Vores nuværende ExecutorService er enkelt gevind, da den blev opnået med Executors.newSingleThreadExecutor. For at fremhæve denne “single threadness”, lad os udløse to beregninger samtidigt:

SquareCalculator squareCalculator = ny SquareCalculator (); Fremtidig fremtid1 = squareCalculator.calculate (10); Future future2 = squareCalculator.calculate (100); mens (! (future1.isDone () && future2.isDone ())) {System.out.println (String.format ("future1 er% s og future2 er% s", future1.isDone ()? "gjort": "ikke færdig", future2.isDone ()? "gjort": "ikke færdig")); Tråd. Søvn (300); } Heltalsresultat1 = fremtid1.get (); Heltalsresultat2 = future2.get (); System.out.println (resultat1 + "og" + resultat2); squareCalculator.shutdown ();

Lad os nu analysere output for denne kode:

beregning af kvadrat for: 10 fremtid1 er ikke færdig og fremtid 2 er ikke færdig fremtid 1 er ikke færdig og fremtid 2 er ikke færdig fremtid 1 er ikke færdig og fremtid 2 er ikke færdig fremtid 1 er ikke færdig og fremtid 2 er ikke færdig fremtid2 er ikke færdig fremtid1 er færdig og fremtid2 er ikke færdig fremtid1 er færdig og fremtid2 er ikke færdig 100 og 10000

Det er klart, at processen ikke er parallel. Læg mærke til, hvordan den anden opgave kun starter, når den første opgave er afsluttet, hvilket får hele processen til at tage ca. 2 sekunder at afslutte.

For at gøre vores program virkelig flertrådet bør vi bruge en anden smag af ExecutorService. Lad os se, hvordan opførslen i vores eksempel ændres, hvis vi bruger en trådpulje, leveret af fabriksmetoden Executors.newFixedThreadPool ():

offentlig klasse SquareCalculator {private ExecutorService eksekutor = Executors.newFixedThreadPool (2); // ...}

Med en simpel ændring i vores SquareCalculator klasse nu har vi en eksekutor, der er i stand til at bruge 2 samtidige tråde.

Hvis vi kører den nøjagtige samme klientkode igen, får vi følgende output:

beregning af firkant for: 10 beregning af firkant for: 100 fremtid1 er ikke færdig og fremtid2 er ikke færdig fremtid1 er ikke færdig og fremtid2 er ikke færdig fremtid1 er ikke færdig og fremtid2 er ikke færdig

Dette ser meget bedre ud nu. Læg mærke til, hvordan de to opgaver starter og slutter at køre samtidigt, og hele processen tager ca. 1 sekund at gennemføre.

Der er andre fabriksmetoder, der kan bruges til at oprette trådpuljer, som f.eks Executors.newCachedThreadPool () der genbruger tidligere brugt Tråds når de er tilgængelige, og Executors.newScheduledThreadPool () som planlægger kommandoer til at køre efter en given forsinkelse.

For mere information om ExecutorService, læs vores artikel dedikeret til emnet.

5. Oversigt over ForkJoinTask

ForkJoinTask er en abstrakt klasse, der implementeres Fremtid og er i stand til at køre et stort antal opgaver, der hostes af et lille antal faktiske tråde i ForkJoinPool.

I dette afsnit skal vi hurtigt dække de vigtigste egenskaber ved ForkJoinPool. For en omfattende vejledning om emnet, se vores guide til gaffel / deltag ramme i Java.

Derefter det vigtigste kendetegn ved en ForkJoinTask er, at det normalt vil give nye underopgaver som en del af det arbejde, der kræves for at fuldføre dets hovedopgave. Det genererer nye opgaver ved at ringe gaffel() og det samler alle resultater med tilslutte(), dermed navnet på klassen.

Der er to abstrakte klasser, der implementeres ForkJoinTask: Rekursiv opgave der returnerer en værdi efter afslutningen, og Rekursiv handling som ikke returnerer noget. Som navnene antyder, skal disse klasser bruges til rekursive opgaver, som for eksempel filsystemnavigation eller kompleks matematisk beregning.

Lad os udvide vores tidligere eksempel for at oprette en klasse, der får en Heltal, beregner sum kvadraterne for alle dens faktuelle elementer. Så hvis vi for eksempel sender nummeret 4 til vores lommeregner, skal vi få resultatet fra summen af ​​4² + 3² + 2² + 1², som er 30.

Først og fremmest er vi nødt til at skabe en konkret implementering af Rekursiv opgave og implementere dens beregne () metode. Det er her, vi skriver vores forretningslogik:

offentlig klasse FactorialSquareCalculator udvider RecursiveTask {privat heltal n; public FactorialSquareCalculator (Integer n) {this.n = n; } @ Override-beskyttet heltal beregne () {hvis (n <= 1) {return n; } FactorialSquareCalculator calculator = ny FactorialSquareCalculator (n - 1); lommeregner. gaffel (); returner n * n + calculator.join (); }}

Læg mærke til, hvordan vi opnår rekursivitet ved at skabe en ny forekomst af FactorialSquareCalculator inden for beregne (). Ved at ringe gaffel(), en ikke-blokerende metode, spørger vi ForkJoinPool at starte udførelsen af ​​denne delopgave.

Det tilslutte() metoden returnerer resultatet fra denne beregning, hvortil vi tilføjer kvadratet af det antal, vi i øjeblikket besøger.

Nu skal vi bare oprette en ForkJoinPool til at håndtere udførelse og trådadministration:

ForkJoinPool forkJoinPool = ny ForkJoinPool (); FactorialSquareCalculator calculator = ny FactorialSquareCalculator (10); forkJoinPool.execute (lommeregner);

6. Konklusion

I denne artikel havde vi en samlet oversigt over Fremtid interface, besøger alle dens metoder. Vi har også lært, hvordan man udnytter kraften fra trådpuljer til at udløse flere parallelle operationer. De vigtigste metoder fra ForkJoinTask klasse, gaffel() og tilslutte() blev også kort dækket.

Vi har mange andre gode artikler om parallelle og asynkrone operationer i Java. Her er tre af dem, der er tæt knyttet til Fremtid interface (nogle af dem er allerede nævnt i artiklen):

  • Vejledning til Fuldført - en implementering af Fremtid med mange ekstra funktioner introduceret i Java 8
  • Guide til Fork / Join Framework i Java - mere om ForkJoinTask vi dækkede i afsnit 5
  • Vejledning til Java ExecutorService - dedikeret til ExecutorService interface

Tjek kildekoden, der er brugt i denne artikel i vores GitHub-lager.


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