Vodič za java.util.concurrent.Future

1. Pregled

U ovom ćemo članku saznati više o tome Budućnost. Sučelje koje postoji od Jave 1.5 i može biti vrlo korisno u radu s asinkronim pozivima i istodobnom obradom.

2. Stvaranje Budućnosti

Jednostavno rečeno, Budućnost klasa predstavlja budući rezultat asinkronog izračunavanja - rezultat koji će se na kraju pojaviti u Budućnost nakon završetka obrade.

Pogledajmo kako napisati metode koje stvaraju i vraćaju a Budućnost primjer.

Dugotrajne metode dobri su kandidati za asinkronu obradu i Budućnost sučelje. To nam omogućuje izvršavanje nekog drugog postupka dok čekamo zadatak u koji je uvršten Budućnost dovršiti.

Neki primjeri operacija koje bi iskoristile asinkronu prirodu Budućnost su:

  • računalno intenzivni procesi (matematički i znanstveni proračuni)
  • manipuliranje strukturama velikih podataka (veliki podaci)
  • pozivi udaljenih metoda (preuzimanje datoteka, uklanjanje HTML-a, web usluge).

2.1. Provedba Budućnosti S FutureTask

Za naš primjer stvorit ćemo vrlo jednostavnu klasu koja izračunava kvadrat an Cijeli broj. Ovo definitivno ne odgovara kategoriji "dugotrajnih" metoda, ali stavit ćemo Thread.sleep () pozovite ga da traje 1 sekundu da završi:

javna klasa SquareCalculator {private ExecutorService izvršitelj = Izvršitelji.newSingleThreadExecutor (); javni Izračun budućnosti (Integer input) {return executor.submit (() -> {Thread.sleep (1000); return input * input;}); }}

Bit koda koji stvarno izvodi proračun nalazi se u poziv() metoda, isporučena kao lambda izraz. Kao što vidite, u tome nema ništa posebno, osim u spavati() poziv spomenut ranije.

Postaje zanimljivije kad svoju pozornost usmjerimo na upotrebu Pozivno i ExecutorService.

Pozivno je sučelje koje predstavlja zadatak koji vraća rezultat i ima jedan poziv() metoda. Ovdje smo stvorili njegovu primjerak pomoću lambda izraza.

Stvaranje instance Pozivno ne vodi nas nikamo, još uvijek moramo proslijediti ovu instancu izvršitelju koji će se pobrinuti za pokretanje tog zadatka u novoj niti i vratiti nam dragocjeno Budućnost objekt. To je gdje ExecutorService ulazi.

Postoji nekoliko načina na koje možemo doći do ExecutorService Primjerice, većinu njih pruža klasa korisnosti Izvršitelji statičke tvorničke metode. U ovom smo primjeru koristili osnovno newSingleThreadExecutor (), koji nam daje ExecutorService sposoban rukovati jednom nitom odjednom.

Jednom kad imamo ExecutorService objekt, samo trebamo nazvati podnijeti() prolazeći naš Pozivno kao argument. podnijeti() pobrinut će se za pokretanje zadatka i vratiti a FutureTask objekt, što je provedba Budućnost sučelje.

3. Konzumirajući Budućnosti

Do ovog trenutka naučili smo kako stvoriti instancu Budućnost.

U ovom ćemo odjeljku naučiti kako raditi s ovom instancom istražujući sve metode koje su dio BudućnostAPI-ja.

3.1. Koristeći Gotovo je() i dobiti() za dobivanje rezultata

Sad moramo nazvati izračunati() i koristite vraćeno Budućnost da se dobije rezultirajuće Cijeli broj. Dvije metode iz Budućnost API će nam pomoći u ovom zadatku.

Future.isDone () govori nam je li izvršitelj završio obradu zadatka. Ako je zadatak dovršen, vratit će se pravi u suprotnom, vraća se lažno.

Metoda koja vraća stvarni rezultat iz izračuna je Future.get (). Primijetite da ova metoda blokira izvršenje dok se zadatak ne dovrši, ali u našem primjeru to neće predstavljati problem jer ćemo prvo provjeriti je li zadatak dovršen pozivom Gotovo je().

Korištenjem ove dvije metode možemo pokrenuti neki drugi kod dok čekamo da glavni zadatak završi:

Buduća budućnost = novi SquareCalculator (). Izračunaj (10); while (! future.isDone ()) {System.out.println ("Izračunavanje ..."); Navoj.spavanje (300); } Cjelobrojni rezultat = future.get ();

U ovom primjeru na izlaz napišemo jednostavnu poruku kako bismo obavijestili korisnika da program izvodi proračun.

Metoda dobiti() blokirat će izvršenje dok zadatak ne bude dovršen. Ali ne moramo se brinuti oko toga, jer naš primjer dolazi samo do točke gdje dobiti() poziva se nakon što se osigura da je zadatak završen. Dakle, u ovom scenariju, future.get () uvijek će se odmah vratiti.

Vrijedno je to spomenuti dobiti() ima preopterećenu verziju koja uzima vremensko ograničenje i TimeUnit kao argumenti:

Cjelobrojni rezultat = future.get (500, TimeUnit.MILLISECONDS);

Razlika između get (long, TimeUnit) i dobiti(), je da će prvi baciti a TimeoutException ako se zadatak ne vrati prije određenog vremenskog ograničenja.

3.2. Otkazivanje a Budućnost Wi otkazati()

Pretpostavimo da smo pokrenuli zadatak, ali nam iz nekog razloga više nije stalo do rezultata. Možemo koristiti Future.cancel (boolean) reći izvršitelju da zaustavi operaciju i prekine njezinu temeljnu nit:

Buduća budućnost = novi SquareCalculator (). Izračunaj (4); logička vrijednost otkazana = future.cancel (true);

Naš primjer Budućnost iz gornjeg koda nikada ne bi dovršio svoj rad. Zapravo, ako pokušamo nazvati dobiti() iz te instance, nakon poziva na otkazati(), ishod bi bio a CancellationException. Future.isCancelled () će nam reći ako a Budućnost je već otkazan. Ovo može biti vrlo korisno kako biste izbjegli dobivanje a CancellationException.

Moguće je da poziv na otkazati() ne uspije. U tom će slučaju njegova vraćena vrijednost biti lažno. Primijeti da otkazati() traje a boolean vrijednost kao argument - ovo kontrolira hoće li nit koja izvršava ovaj zadatak biti prekinuta ili ne.

4. Više Multithreading sa Nit Bazeni

Naša struja ExecutorService je jednostruki navoj jer je dobiven s izvršiteljima.newSingleThreadExecutor. Da bismo istakli ovu "jednostruku nitnost", pokrenimo istovremeno dva izračuna:

SquareCalculator squareCalculator = novi SquareCalculator (); Buduća budućnost1 = squareCalculator.calculate (10); Buduća budućnost2 = squareCalculator.calculate (100); while (! (future1.isDone () && future2.isDone ())) {System.out.println (String.format ("future1 je% s, a future2 je% s", future1.isDone ()? "done"): "nije gotovo", future2.isDone ()? "done": "nije gotovo")); Navoj.spavanje (300); } Cijeli rezultat1 = future1.get (); Cijeli rezultat2 = future2.get (); System.out.println (rezultat1 + "i" + rezultat2); squareCalculator.shutdown ();

Sada analizirajmo izlaz za ovaj kod:

izračunavanje kvadrata za: 10 budućih1 nije učinjeno i buduće2 nije učinjeno buduće1 nije učinjeno i buduće2 nije učinjeno buduće1 nije učinjeno i buduće2 nije učinjeno buduće1 nije izvršeno i buduće2 nije gotovo izračunavanje kvadrata za: 100 budućih1 je učinjeno i future2 nije učinjeno future1 je učinjeno i future2 nije učinjeno future1 je učinjeno i future2 nije učinjeno 100 i 10000

Jasno je da postupak nije paralelan. Primijetite kako drugi zadatak započinje tek kad je prvi zadatak dovršen, čineći da cijeli postupak traje oko 2 sekunde.

Da bi naš program bio doista višenitni, trebali bismo koristiti drugačiji okus ExecutorService. Pogledajmo kako se ponašanje našeg primjera mijenja ako koristimo spremište niti, osigurano tvorničkom metodom Izvršitelji.newFixedThreadPool ():

javna klasa SquareCalculator {private ExecutorService izvršitelj = Izvršitelji.newFixedThreadPool (2); // ...}

Uz jednostavnu promjenu u našem SquareCalculator klase sada imamo izvršitelja koji može koristiti 2 simultane niti.

Ako ponovno pokrenemo isti klijentski kod, dobit ćemo sljedeći izlaz:

izračunavanje kvadrata za: 10 izračunavanje kvadrata za: 100 budućnost1 nije gotovo i budućnost2 nije učinjeno budućnost1 nije učinjeno i budućnost2 nije učinjeno budućnost1 nije učinjeno i budućnost2 nije učinjeno budućnost1 nije učinjeno i budućnost2 nije učinjeno 100 i 10000

Ovo sada izgleda puno bolje. Primijetite kako se dva zadatka istovremeno pokreću i završavaju, a cijeli postupak traje oko 1 sekunde.

Postoje i druge tvorničke metode koje se mogu koristiti za stvaranje spremišta niti, poput Izvršitelji.newCachedThreadPool () koja ponovno koristi prethodno korištene Nits kad su dostupni i Izvršitelji.newScheduledThreadPool () koji raspoređuje naredbe za pokretanje nakon određenog kašnjenja.

Za više informacija o ExecutorService, pročitajte naš članak posvećen toj temi.

5. Pregled ForkJoinTask

ForkJoinTask je apstraktna klasa koja provodi Budućnost i sposoban je izvoditi velik broj zadataka koje hostira mali broj stvarnih niti u sustavu Windows ForkJoinPool.

U ovom ćemo odjeljku brzo pokriti glavne karakteristike ForkJoinPool. Sveobuhvatan vodič o temi potražite u našem Vodiču za Fork / Join Framework na Javi.

Tada je glavna karakteristika a ForkJoinTask je da će obično iznjedriti nove podzadaće kao dio posla potrebnog za dovršavanje svoje glavne zadaće. Pozivima generira nove zadatke vilica () i okuplja sve rezultate s pridružiti(), dakle naziv razreda.

Postoje dvije apstraktne klase koje se provode ForkJoinTask: Rekurzivna zadaća koji vraća vrijednost po završetku, i Rekurzivno djelovanje koja ne vraća ništa. Kao što nazivi impliciraju, te se klase trebaju koristiti za rekurzivne zadatke, poput primjerice navigacije datotečnim sustavom ili složenih matematičkih izračuna.

Proširimo naš prethodni primjer kako bismo stvorili klasu koja, s obzirom na Cijeli broj, izračunat će kvadrate zbroja za sve njegove faktorijelne elemente. Tako, na primjer, ako prenesemo broj 4 na naš kalkulator, trebali bismo dobiti rezultat iz zbroja 4² + 3² + 2² + 1² koji je 30.

Prije svega, moramo stvoriti konkretnu provedbu Rekurzivna zadaća i provesti svoje izračunati () metoda. Ovdje ćemo napisati našu poslovnu logiku:

javna klasa FactorialSquareCalculator proširuje RecursiveTask {private Integer n; javni FactorialSquareCalculator (cijeli broj n) {this.n = n; } @Override protected Integer compute () {if (n <= 1) {return n; } FactorialSquareCalculator kalkulator = novi FactorialSquareCalculator (n - 1); kalkulator.fork (); povratak n * n + kalkulator.join (); }}

Primijetite kako postižemo rekurzivnost stvaranjem nove instance FactorialSquareCalculator unutar izračunati (). Pozivanjem vilica (), neblokirajuća metoda, pitamo ForkJoinPool za pokretanje izvršenja ovog podzadatka.

The pridružiti() metoda vratit će rezultat iz tog izračuna, kojem dodamo kvadrat broja koji trenutno posjećujemo.

Sada samo trebamo stvoriti ForkJoinPool za rukovanje izvršenjem i upravljanjem nitima:

ForkJoinPool forkJoinPool = novi ForkJoinPool (); FactorialSquareCalculator kalkulator = novi FactorialSquareCalculator (10); forkJoinPool.execute (kalkulator);

6. Zaključak

U ovom smo članku imali sveobuhvatan pogled na Budućnost sučelje, obilazeći sve njegove metode. Također smo naučili kako iskoristiti snagu spremišta niti za pokretanje više paralelnih operacija. Glavne metode iz ForkJoinTask razred, vilica () i pridružiti() bili kratko i pokriveni.

Imamo mnogo drugih sjajnih članaka o paralelnim i asinkronim operacijama u Javi. Evo tri od njih koja su usko povezana s Budućnost sučelje (neki od njih su već spomenuti u članku):

  • Vodič za CompletableFuture - provedba Budućnost s mnogim dodatnim značajkama uvedenim u Javi 8
  • Vodič za Fork / Join Framework u Javi - više o ForkJoinTask obradili smo u odjeljku 5
  • Vodič za Javu ExecutorService - posvećen ExecutorService sučelje

Provjerite izvorni kod korišten u ovom članku u našem GitHub spremištu.