Gids voor java.util.concurrent.Future

1. Overzicht

In dit artikel gaan we meer leren over Toekomst. Een interface die al bestaat sinds Java 1.5 en erg handig kan zijn bij het werken met asynchrone oproepen en gelijktijdige verwerking.

2. Creëren Futures

Simpel gezegd, de Toekomst class vertegenwoordigt een toekomstig resultaat van een asynchrone berekening - een resultaat dat uiteindelijk zal verschijnen in de Toekomst nadat de verwerking is voltooid.

Laten we eens kijken hoe we methoden kunnen schrijven die een Toekomst voorbeeld.

Langlopende methoden zijn goede kandidaten voor asynchrone verwerking en de Toekomst koppel. Dit stelt ons in staat om een ​​ander proces uit te voeren terwijl we wachten op de ingekapselde taak Toekomst vervolledigen.

Enkele voorbeelden van bewerkingen die de asynchrone aard van Toekomst zijn:

  • rekenintensieve processen (wiskundige en wetenschappelijke berekeningen)
  • manipuleren van grote datastructuren (big data)
  • externe methodeaanroepen (downloaden van bestanden, HTML-scrapping, webservices).

2.1. Implementeren Futures Met FutureTask

Voor ons voorbeeld gaan we een heel eenvoudige klasse maken die het kwadraat van een berekent Geheel getal. Dit past absoluut niet in de categorie "langlopende" methoden, maar we gaan een Thread.sleep () roep ernaar om het 1 seconde te laten duren om te voltooien:

openbare klasse SquareCalculator {privé ExecutorService-uitvoerder = Executors.newSingleThreadExecutor (); openbaar Future berekenen (Integer invoer) {return executor.submit (() -> {Thread.sleep (1000); return input * input;}); }}

Het stukje code dat de berekening daadwerkelijk uitvoert, bevindt zich in de bellen () methode, geleverd als een lambda-uitdrukking. Zoals je kunt zien is er niets speciaals aan, behalve de slaap() eerder genoemde oproep.

Het wordt interessanter als we onze aandacht vestigen op het gebruik van Oproepbaar en ExecutorService.

Oproepbaar is een interface die een taak vertegenwoordigt die een resultaat retourneert en een enkele heeft bellen () methode. Hier hebben we een instantie ervan gemaakt met behulp van een lambda-expressie.

Een instantie maken van Oproepbaar brengt ons nergens heen, we moeten deze instantie nog steeds doorgeven aan een uitvoerder die ervoor zorgt dat die taak in een nieuwe thread wordt gestart en ons de waardevolle Toekomst voorwerp. Dat is waar ExecutorService komt binnen.

Er zijn een paar manieren waarop we een ExecutorService De meeste worden bijvoorbeeld geleverd door de utility-klasse Uitvoerders ‘ statische fabrieksmethoden. In dit voorbeeld hebben we de basis newSingleThreadExecutor (), wat ons een ExecutorService in staat om een ​​enkele thread tegelijk te verwerken.

Zodra we een ExecutorService object, we hoeven alleen maar te bellen indienen () voorbij onze Oproepbaar als argument. indienen () zorgt voor het starten van de taak en retourneert een FutureTask object, wat een implementatie is van het Toekomst koppel.

3. Consumeren Futures

Tot nu toe hebben we geleerd hoe we een instantie van Toekomst.

In deze sectie leren we hoe u met deze instantie kunt werken door alle methoden te verkennen die deel uitmaken van Toekomst‘S API.

3.1. Gebruik makend van is klaar() en krijgen() om resultaten te verkrijgen

Nu moeten we bellen berekenen() en gebruik de geretourneerde Toekomst om het resultaat te krijgen Geheel getal. Twee methoden uit de Toekomst API helpt ons bij deze taak.

Future.isDone () vertelt ons of de uitvoerder klaar is met het verwerken van de taak. Als de taak is voltooid, keert deze terug waar anders keert het terug false.

De methode die het werkelijke resultaat van de berekening retourneert, is Future.get (). Merk op dat deze methode de uitvoering blokkeert totdat de taak is voltooid, maar in ons voorbeeld is dit geen probleem, aangezien we eerst zullen controleren of de taak is voltooid door te bellen is klaar().

Door deze twee methoden te gebruiken, kunnen we een andere code uitvoeren terwijl we wachten tot de hoofdtaak is voltooid:

Toekomstige toekomst = nieuwe SquareCalculator (). Bereken (10); while (! future.isDone ()) {System.out.println ("Berekenen ..."); Thread.sleep (300); } Integer resultaat = future.get ();

In dit voorbeeld schrijven we een eenvoudig bericht op de uitvoer om de gebruiker te laten weten dat het programma de berekening uitvoert.

De methode krijgen() blokkeert de uitvoering totdat de taak is voltooid. Maar daar hoeven we ons geen zorgen over te maken, aangezien ons voorbeeld maar tot het punt komt waar krijgen() wordt aangeroepen nadat u zich ervan heeft vergewist dat de taak is voltooid. Dus in dit scenario future.get () zal altijd onmiddellijk terugkeren.

Dat is het vermelden waard krijgen() heeft een overbelaste versie die een time-out en een TimeUnit als argumenten:

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

Het verschil tussen get (lang, TimeUnit) en krijgen(), is dat de eerste een TimeoutException als de taak niet terugkeert vóór de opgegeven time-outperiode.

3.2. Annuleren van een Toekomstige W.ith annuleren()

Stel dat we een taak hebben geactiveerd, maar om de een of andere reden geven we niet meer om het resultaat. We kunnen gebruiken Future.cancel (boolean) om de uitvoerder te vertellen om de bewerking te stoppen en de onderliggende thread te onderbreken:

Toekomstige toekomst = nieuwe SquareCalculator (). Bereken (4); boolean geannuleerd = future.cancel (true);

Ons exemplaar van Toekomst van de bovenstaande code zou de werking ervan nooit voltooien. In feite, als we proberen te bellen krijgen() vanaf dat moment, na de oproep naar annuleren(), zou het resultaat een zijn Annulering Uitzondering. Future.isCancelled () zal ons vertellen of a Toekomst was al geannuleerd. Dit kan erg handig zijn om te voorkomen dat u een Annulering Uitzondering.

Het is mogelijk dat een telefoontje naar annuleren() mislukt. In dat geval is de geretourneerde waarde false. Let erop dat annuleren() duurt een boolean waarde als een argument - dit bepaalt of de thread die deze taak uitvoert, moet worden onderbroken of niet.

4. Meer multithreading met Draad Zwembaden

Onze huidige ExecutorService is single threaded sinds het werd verkregen met de Executors.newSingleThreadExecutor. Om deze “enkele draadheid” te benadrukken, laten we twee berekeningen tegelijk starten:

SquareCalculator squareCalculator = nieuwe SquareCalculator (); Future future1 = squareCalculator.calculate (10); Future future2 = squareCalculator.calculate (100); while (! (future1.isDone () && future2.isDone ())) {System.out.println (String.format ("future1 is% s en future2 is% s", future1.isDone ()? "done": "not done", future2.isDone ()? "done": "not done")); Thread.sleep (300); } Geheel getal resultaat1 = toekomst1.get (); Geheel getal resultaat2 = toekomst2.get (); System.out.println (resultaat1 + "en" + resultaat2); squareCalculator.shutdown ();

Laten we nu de uitvoer voor deze code analyseren:

kwadraat berekenen voor: 10 future1 is not done en future2 is not done future1 is not done en future2 is not done future1 is not done en future2 is not done future1 is not done en future2 is not done kwadraat berekenen voor: 100 future1 is klaar en future2 is not done future1 is klaar en future2 is not done future1 is klaar en future2 is not done 100 en 10000

Het is duidelijk dat het proces niet parallel verloopt. Merk op dat de tweede taak pas begint als de eerste taak is voltooid, waardoor het hele proces ongeveer 2 seconden duurt om te voltooien.

Om ons programma echt multi-threaded te maken, moeten we een andere smaak gebruiken van ExecutorService. Laten we eens kijken hoe het gedrag van ons voorbeeld verandert als we een threadpool gebruiken, geleverd door de fabrieksmethode Executors.newFixedThreadPool ():

openbare klasse SquareCalculator {privé ExecutorService-uitvoerder = Executors.newFixedThreadPool (2); // ...}

Met een simpele wijziging in ons SquareCalculator class nu hebben we een uitvoerder die 2 gelijktijdige threads kan gebruiken.

Als we exact dezelfde clientcode opnieuw uitvoeren, krijgen we de volgende uitvoer:

kwadraat berekenen voor: 10 kwadraat berekenen voor: 100 future1 is not done en future2 is not done future1 is not done en future2 is not done future1 is not done en future2 is niet gedaan future1 is not done en future2 is niet gedaan 100 en 10000

Dit ziet er nu veel beter uit. Merk op hoe de 2 taken tegelijkertijd beginnen en eindigen, en het hele proces duurt ongeveer 1 seconde om te voltooien.

Er zijn andere fabrieksmethoden die kunnen worden gebruikt om threadpools te maken, zoals Executors.newCachedThreadPool () die eerder gebruikt hergebruikt Draads wanneer ze beschikbaar zijn, en Executors.newScheduledThreadPool () die de opdrachten plant die na een bepaalde vertraging moeten worden uitgevoerd.

Voor meer informatie over ExecutorService, lees ons artikel over het onderwerp.

5. Overzicht van ForkJoinTask

ForkJoinTask is een abstracte klasse die implementeert Toekomst en kan een groot aantal taken uitvoeren die worden gehost door een klein aantal daadwerkelijke threads in ForkJoinPool.

In dit gedeelte gaan we snel de belangrijkste kenmerken van ForkJoinPool. Raadpleeg onze Gids voor het Fork / Join Framework in Java voor een uitgebreide gids over het onderwerp.

Dan is het belangrijkste kenmerk van a ForkJoinTask is dat het gewoonlijk nieuwe subtaken zal voortbrengen als onderdeel van het werk dat nodig is om zijn hoofdtaak te voltooien. Het genereert nieuwe taken door te bellen vork() en het verzamelt alle resultaten met toetreden (), dus de naam van de klas.

Er zijn twee abstracte klassen die implementeren ForkJoinTask: RecursiveTask die een waarde retourneert bij voltooiing, en RecursiveAction die niets retourneert. Zoals de namen impliceren, moeten die klassen worden gebruikt voor recursieve taken, zoals bijvoorbeeld bestandssysteemnavigatie of complexe wiskundige berekeningen.

Laten we ons vorige voorbeeld uitbreiden om een ​​klasse te maken die, gegeven een Geheel getal, berekent de som-kwadraten voor al zijn faculteit-elementen. Als we bijvoorbeeld het getal 4 doorgeven aan onze rekenmachine, zouden we het resultaat moeten krijgen van de som van 4² + 3² + 2² + 1², wat 30 is.

Allereerst moeten we een concrete implementatie maken van RecursiveTask en implementeer zijn berekenen() methode. Dit is waar we onze bedrijfslogica zullen schrijven:

openbare klasse FactorialSquareCalculator breidt RecursiveTask {private Integer n; openbare FactorialSquareCalculator (geheel getal n) {this.n = n; } @Override protected Integer compute () {if (n <= 1) {return n; } FactorialSquareCalculator calculator = nieuwe FactorialSquareCalculator (n - 1); calculator.fork (); retourneer n * n + calculator.join (); }}

Merk op hoe we recursiviteit bereiken door een nieuw exemplaar van FactorialSquareCalculator binnen berekenen(). Door te bellen vork(), een niet-blokkerende methode, vragen we ForkJoinPool om de uitvoering van deze subtaak te starten.

De toetreden () methode retourneert het resultaat van die berekening, waaraan we het kwadraat toevoegen van het nummer dat we momenteel bezoeken.

Nu hoeven we alleen maar een ForkJoinPool om de uitvoering en het threadbeheer af te handelen:

ForkJoinPool forkJoinPool = nieuwe ForkJoinPool (); FactorialSquareCalculator calculator = nieuwe FactorialSquareCalculator (10); forkJoinPool.execute (rekenmachine);

6. Conclusie

In dit artikel hadden we een uitgebreid overzicht van de Toekomst interface, door al zijn methoden te bezoeken. We hebben ook geleerd hoe we de kracht van threadpools kunnen gebruiken om meerdere parallelle bewerkingen te activeren. De belangrijkste methoden van de ForkJoinTask klasse, vork() en toetreden () werden ook kort behandeld.

We hebben veel andere geweldige artikelen over parallelle en asynchrone bewerkingen in Java. Hier zijn er drie die nauw verwant zijn aan de Toekomst interface (sommige worden al genoemd in het artikel):

  • Gids naar CompletableFuture - een implementatie van Toekomst met veel extra functies geïntroduceerd in Java 8
  • Gids voor het Fork / Join Framework in Java - meer over ForkJoinTask we behandelden in sectie 5
  • Gids voor de Java ExecutorService - gewijd aan de ExecutorService koppel

Controleer de broncode die in dit artikel wordt gebruikt in onze GitHub-repository.