Java >> Java tutorial >  >> Tag >> java.util

Guide til java.util.concurrent.Future

1. Oversigt

I dette selvstudie lærer vi om Fremtiden . En grænseflade, der har eksisteret siden Java 1.5, kan være ganske nyttig, når du arbejder med asynkrone opkald og samtidig behandling.

2. Oprettelse af Futures

Kort sagt, Fremtiden klasse repræsenterer et fremtidigt resultat af en asynkron beregning. Dette resultat vil til sidst dukke op i Fremtiden efter at behandlingen er fuldført.

Lad os se, hvordan man skriver metoder, der skaber og returnerer en Fremtid eksempel.

Langvarige metoder er gode kandidater til asynkron behandling og Fremtiden grænseflade, fordi vi kan udføre andre processer, mens vi venter på opgaven indkapslet i Fremtiden at fuldføre.

Nogle eksempler på operationer, der ville udnytte Fremtidenss asynkrone natur er:

  • beregningsintensive processer (matematiske og videnskabelige beregninger)
  • manipulation af store datastrukturer (big data)
  • fjernmetodekald (download af filer, HTML-skrotning, webtjenester)

2.1. Implementering af Futures Med FutureTask

For vores eksempel vil vi oprette en meget simpel klasse, der beregner kvadratet af et heltal . Dette passer bestemt ikke til kategorien langvarige metoder, men vi vil sætte en Thread.sleep() kald til det, så det varer 1 sekund, før du fuldfører:

public class SquareCalculator {    
    
    private ExecutorService executor 
      = Executors.newSingleThreadExecutor();
    
    public Future<Integer> calculate(Integer input) {        
        return executor.submit(() -> {
            Thread.sleep(1000);
            return input * input;
        });
    }
}

Den kodebit, der rent faktisk udfører beregningen, er indeholdt i call() metode, og leveres som et lambda-udtryk. Som vi kan se, er der ikke noget særligt ved det, bortset fra sleep() opkald nævnt tidligere.

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

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

Oprettelse af en forekomst af Callable bringer os ikke nogen steder; vi er stadig nødt til at videregive denne instans til en udfører, der vil tage sig af at starte opgaven i en ny tråd, og give os den værdifulde Fremtid tilbage objekt. Det er her ExecutorService kommer ind.

Der er et par måder, hvorpå vi kan få adgang til en ExecutorService instans, og de fleste af dem leveres af hjælpeklassen Executors' statiske fabriksmetoder. I dette eksempel brugte vi den grundlæggende newSingleThreadExecutor() , som giver os en ExecutorService i stand til at håndtere en enkelt tråd ad gangen.

Når vi har en ExecutorService objekt, skal vi bare kalde submit(), passerer vores Opkaldbare som et argument. Derefter submit() vil starte opgaven og returnere en FutureTask objekt, som er en implementering af Fremtiden grænseflade.

3. Forbruger Futures

Indtil nu har vi lært, hvordan man opretter en forekomst af Future .

I dette afsnit lærer vi, hvordan du arbejder med denne instans ved at udforske alle de metoder, der er en del af Fremtiden 's API.

3.1. Brug af isDone() og get() for at opnå resultater

Nu skal vi kalde calculate(), og brug den returnerede Fremtid for at få det resulterende heltal . To metoder fra Fremtiden API vil hjælpe os med denne opgave.

Future.isDone() fortæller os, om udføreren er færdig med at behandle opgaven. Hvis opgaven er fuldført, returnerer den true; ellers returnerer den falsk .

Metoden, der returnerer det faktiske resultat fra beregningen, er Future.get() . Vi kan se, at denne metode blokerer udførelsen, indtil opgaven er fuldført. Dette vil dog ikke være et problem i vores eksempel, fordi vi tjekker, om opgaven er fuldført ved at kalde isDone() .

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

Future<Integer> future = new SquareCalculator().calculate(10);

while(!future.isDone()) {
    System.out.println("Calculating...");
    Thread.sleep(300);
}

Integer result = future.get();

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

Metoden get() blokerer udførelsen, indtil opgaven er fuldført. Igen, dette vil ikke være et problem, fordi i vores eksempel, get() vil først blive kaldt efter at have sikret sig, at opgaven er afsluttet. Så i dette scenarie, future.get() vender altid tilbage med det samme.

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

Integer result = future.get(500, TimeUnit.MILLISECONDS);

Forskellen mellem get(long, TimeUnit) og get() er, at førstnævnte vil kaste en TimeoutException hvis opgaven ikke vender tilbage inden den angivne timeoutperiode.

3.2. Annullering af en Fremtid Med cancel()

Antag, at vi udløste en opgave, men af ​​en eller anden grund er vi ligeglade med resultatet længere. Vi kan bruge Future.cancel(boolean) at bede udføreren om at stoppe operationen og afbryde dens underliggende tråd:

Future<Integer> future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

Vores eksempel på Future, fra koden ovenfor, vil aldrig fuldføre sin funktion. Faktisk, hvis vi forsøger at kalde get() fra dette tilfælde efter opkaldet til annuller() , vil resultatet være en Annulleringsundtagelse . Future.isCancelled() vil fortælle os, om en Fremtid var allerede aflyst. Dette kan være ret nyttigt for at undgå at få en Annullationsundtagelse .

Det er også muligt, at et opkald til annuller() fejler. I så fald vil den returnerede værdi være false . Det er vigtigt at bemærke, at cancel() tager en boolsk værdi som argument. Dette styrer, om tråden, der udfører opgaven, skal afbrydes eller ej.

4. Mere multithreading med Thread Pools

Vores nuværende ExecutorService er enkelttrådet, da det blev opnået med Executors.newSingleThreadExecutor. For at fremhæve denne enkelte tråd, lad os udløse to beregninger samtidigt:

SquareCalculator squareCalculator = new SquareCalculator();

Future<Integer> future1 = squareCalculator.calculate(10);
Future<Integer> future2 = squareCalculator.calculate(100);

while (!(future1.isDone() && future2.isDone())) {
    System.out.println(
      String.format(
        "future1 is %s and future2 is %s", 
        future1.isDone() ? "done" : "not done", 
        future2.isDone() ? "done" : "not done"
      )
    );
    Thread.sleep(300);
}

Integer result1 = future1.get();
Integer result2 = future2.get();

System.out.println(result1 + " and " + result2);

squareCalculator.shutdown();

Lad os nu analysere outputtet for denne kode:

calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000

Det er klart, at processen ikke er parallel. Vi kan se, at den anden opgave først starter, når den første opgave er fuldført, hvilket gør, at hele processen tager omkring 2 sekunder at afslutte.

For at gøre vores program virkelig multi-threaded, bør vi bruge en anden variant af ExecutorService . Lad os se, hvordan adfærden i vores eksempel ændrer sig, hvis vi bruger en trådpulje leveret af fabriksmetoden Executors.newFixedThreadPool() :

public class SquareCalculator {
 
    private ExecutorService executor = Executors.newFixedThreadPool(2);
    
    //...
}

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

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

calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000

Det ser meget bedre ud nu. Vi kan se, at de 2 opgaver starter og slutter med at køre samtidigt, og hele processen tager omkring 1 sekund at fuldføre.

Der er andre fabriksmetoder, der kan bruges til at oprette trådpuljer, såsom Executors.newCachedThreadPool(), som genbruger tidligere brugt tråd s, 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, som implementerer Future, og er i stand til at køre et stort antal opgaver hostet af et lille antal faktiske tråde i ForkJoinPool .

I dette afsnit vil vi hurtigt dække hovedkarakteristikaene ved ForkJoinPool . For en omfattende guide om emnet, se vores guide til Fork/Join Framework i Java.

Hovedkarakteristikken for en ForkJoinTask er, at det normalt vil afføde nye underopgaver som en del af det arbejde, der kræves for at fuldføre dens hovedopgave. Det genererer nye opgaver ved at kalde fork(), og det samler alle resultater med join(), dermed navnet på klassen.

Der er to abstrakte klasser, der implementerer ForkJoinTask :Rekursiv opgave, som returnerer en værdi ved afslutning, og RecursiveAction, som ikke returnerer noget. Som deres navne antyder, skal disse klasser bruges til rekursive opgaver, såsom filsystemnavigation eller kompleks matematisk beregning.

Lad os udvide vores tidligere eksempel for at skabe en klasse, der givet et heltal , beregner sumkvadraterne for alle dens faktorelementer. Så hvis vi for eksempel sender tallet 4 til vores lommeregner, skulle vi få resultatet fra summen af ​​4² + 3² + 2² + 1², hvilket er 30.

Først skal vi skabe en konkret implementering af RecursiveTask og implementer dens compute() metode. Det er her, vi skriver vores forretningslogik:

public class FactorialSquareCalculator extends RecursiveTask<Integer> {
 
    private Integer n;

    public FactorialSquareCalculator(Integer n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if (n <= 1) {
            return n;
        }

        FactorialSquareCalculator calculator 
          = new FactorialSquareCalculator(n - 1);

        calculator.fork();

        return n * n + calculator.join();
    }
}

Læg mærke til, hvordan vi opnår rekursivitet ved at oprette en ny forekomst af FactorialSquareCalculator i compute() . Ved at kalde fork() , en ikke-blokerende metode, spørger vi ForkJoinPool for at starte udførelsen af ​​denne underopgave.

join() metoden returnerer resultatet fra den beregning, hvortil vi tilføjer kvadratet af det tal, vi besøger i øjeblikket.

Nu mangler vi bare at oprette en ForkJoinPool for at håndtere eksekveringen og trådhåndteringen:

ForkJoinPool forkJoinPool = new ForkJoinPool();

FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);

forkJoinPool.execute(calculator);

6. Konklusion

I denne artikel udforskede vi Fremtiden grundigt grænseflade, der berører alle dens metoder. Vi lærte også, hvordan man udnytter kraften i trådpuljer til at udløse flere parallelle operationer. De vigtigste metoder fra ForkJoinTask klasse, fork() og join(), 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 Fremtiden grænseflade, hvoraf nogle allerede er nævnt i artiklen:

  • Guide til CompletableFuture – en implementering af Fremtiden med mange ekstra funktioner introduceret i Java 8
  • Guide til Fork/Join Framework i Java – mere om ForkJoinTask vi dækkede i afsnit 5
  • Guide til Java ExecutorService – dedikeret til ExecutorService grænseflade

Som altid kan kildekoden, der bruges i denne artikel, findes i vores GitHub-lager.


Java tag