Een gids voor de Java ExecutorService

1. Overzicht

ExecutorService is een raamwerk van de JDK dat de uitvoering van taken in asynchrone modus vereenvoudigt. In het algemeen, ExecutorService biedt automatisch een pool van threads en API om er taken aan toe te wijzen.

2. Instantiërend ExecutorService

2.1. Fabrieksmethoden van de Uitvoerders Klasse

De gemakkelijkste manier om te creëren ExecutorService is om een ​​van de fabrieksmethoden van de Uitvoerders klasse.

De volgende regel code maakt bijvoorbeeld een thread-pool met 10 threads:

ExecutorService executor = Executors.newFixedThreadPool (10);

Er zijn verschillende andere fabrieksmethoden om vooraf gedefinieerde ExecutorService die voldoen aan specifieke gebruiksscenario's. Raadpleeg de officiële documentatie van Oracle om de beste methode voor uw behoeften te vinden.

2.2. Maak direct een ExecutorService

Omdat ExecutorService is een interface, een instantie van alle implementaties ervan kan worden gebruikt. Er zijn verschillende implementaties om uit te kiezen in het java.util.concurrent pakket of u kunt uw eigen pakket maken.

Bijvoorbeeld de ThreadPoolExecutor class heeft een paar constructors die kunnen worden gebruikt om een ​​executor-service en zijn interne pool te configureren.

ExecutorService executorService = nieuwe ThreadPoolExecutor (1, 1, 0L, TimeUnit.MILLISECONDS, nieuwe LinkedBlockingQueue ());

Het valt je misschien op dat de bovenstaande code erg lijkt op de broncode van de fabrieksmethode newSingleThreadExecutor (). In de meeste gevallen is een gedetailleerde handmatige configuratie niet nodig.

3. Taken toewijzen aan het ExecutorService

ExecutorService kan uitvoeren Runnable en Oproepbaar taken. Om het in dit artikel eenvoudig te houden, worden twee primitieve taken gebruikt. Merk op dat hier lambda-uitdrukkingen worden gebruikt in plaats van anonieme innerlijke klassen:

Runnable runnableTask = () -> {probeer {TimeUnit.MILLISECONDS.sleep (300); } catch (InterruptedException e) {e.printStackTrace (); }}; Oproepbare callableTask = () -> {TimeUnit.MILLISECONDS.sleep (300); retourneer "uitvoering van de taak"; }; Lijst callableTasks = nieuwe ArrayList (); callableTasks.add (callableTask); callableTasks.add (callableTask); callableTasks.add (callableTask);

Taken kunnen worden toegewezen aan de ExecutorService met behulp van verschillende methoden, waaronder uitvoeren (), die is geërfd van de Uitvoerder interface, en ook indienen (), invokeAny (), invokeAll ().

De uitvoeren () methode is leegte, en het geeft geen enkele mogelijkheid om het resultaat van de uitvoering van een taak te krijgen of om de status van de taak te controleren (of deze wordt uitgevoerd of uitgevoerd).

executorService.execute (runnableTask);

indienen () legt een Oproepbaar of een Runnable taak aan een ExecutorService en retourneert een resultaat van type Toekomst.

Toekomstige toekomst = executorService.submit (callableTask);

invokeAny () wijst een verzameling taken toe aan een ExecutorService, waardoor elk wordt uitgevoerd, en geeft het resultaat van een succesvolle uitvoering van één taak terug (als er een succesvolle uitvoering was).

String resultaat = executorService.invokeAny (callableTasks);

invokeAll () wijst een verzameling taken toe aan een ExecutorService, waardoor ze allemaal worden uitgevoerd, en retourneert het resultaat van alle taakuitvoeringen in de vorm van een lijst met objecten van het type Toekomst.

Lijst futures = executorService.invokeAll (callableTasks);

Nu, voordat we verder gaan, moeten nog twee dingen worden besproken: het afsluiten van een ExecutorService en omgaan met Toekomst retour typen.

4. Een ExecutorService

In het algemeen is de ExecutorService wordt niet automatisch vernietigd als er geen taak is om te verwerken. Het zal in leven blijven en wachten op nieuw werk.

In sommige gevallen is dit erg handig; bijvoorbeeld als een app taken moet verwerken die op een onregelmatige basis verschijnen of als de hoeveelheid van deze taken niet bekend is tijdens het compileren.

Aan de andere kant kan een app zijn einde bereiken, maar deze wordt niet gestopt vanwege een wachttijd ExecutorService zorgt ervoor dat de JVM blijft draaien.

Om een ExecutorService, we hebben de afsluiten() en shutdownNow () API's.

De afsluiten()methode leidt niet tot onmiddellijke vernietiging van de ExecutorService. Het zal de ExecutorService stop met het accepteren van nieuwe taken en sluit af nadat alle actieve threads hun huidige werk hebben voltooid.

executorService.shutdown ();

De shutdownNow () methode probeert de ExecutorService onmiddellijk, maar het garandeert niet dat alle lopende threads tegelijkertijd worden gestopt. Deze methode retourneert een lijst met taken die wachten om te worden verwerkt. Het is aan de ontwikkelaar om te beslissen wat hij met deze taken gaat doen.

Lijst notExecutedTasks = executorService.shutDownNow ();

Een goede manier om het ExecutorService (wat ook wordt aanbevolen door Oracle) is om beide methoden te gebruiken in combinatie met de wachtenTermination () methode. Met deze aanpak kan het ExecutorService stopt eerst met het aannemen van nieuwe taken en wacht vervolgens tot een bepaalde tijd totdat alle taken zijn voltooid. Als die tijd verstrijkt, wordt de uitvoering onmiddellijk gestopt:

executorService.shutdown (); probeer {if (! executorService.awaitTermination (800, TimeUnit.MILLISECONDS)) {executorService.shutdownNow (); }} catch (InterruptedException e) {executorService.shutdownNow (); }

5. Het Toekomst Koppel

De indienen () en invokeAll () methodes retourneren een object of een verzameling objecten van het type Toekomst, waarmee we het resultaat van de uitvoering van een taak kunnen krijgen of de status van de taak kunnen controleren (wordt deze uitgevoerd of uitgevoerd).

De Toekomst interface biedt een speciale blokkeermethode krijgen() die een werkelijk resultaat retourneert van de Oproepbaar uitvoering van de taak of nul in het geval van Runnable taak. Bellen met het krijgen() methode terwijl de taak nog loopt, zal de uitvoering blokkeren totdat de taak correct is uitgevoerd en het resultaat beschikbaar is.

Toekomstige toekomst = executorService.submit (callableTask); String resultaat = null; probeer {resultaat = toekomst.get (); } catch (InterruptedException | ExecutionException e) {e.printStackTrace (); }

Met een zeer lange blokkering veroorzaakt door de krijgen() methode, kunnen de prestaties van een toepassing afnemen. Als de resulterende gegevens niet cruciaal zijn, is het mogelijk om een ​​dergelijk probleem te vermijden door gebruik te maken van time-outs:

String resultaat = future.get (200, TimeUnit.MILLISECONDS);

Als de uitvoeringsperiode langer is dan opgegeven (in dit geval 200 milliseconden), a TimeoutException zal worden gegooid.

De is klaar() methode kan worden gebruikt om te controleren of de toegewezen taak al is verwerkt of niet.

De Toekomst interface voorziet ook in het annuleren van taakuitvoering met de annuleren() methode, en om de annulering te controleren met is geannuleerd() methode:

boolean geannuleerd = future.cancel (true); boolean isCancelled = future.isCancelled ();

6. Het ScheduledExecutorService Koppel

De ScheduledExecutorService voert taken uit na een vooraf gedefinieerde vertraging en / of periodiek. Nogmaals, de beste manier om een ScheduledExecutorService is om de fabrieksmethoden van de Uitvoerders klasse.

Voor deze sectie is een ScheduledExecutorService met één draad wordt gebruikt:

ScheduledExecutorService executorService = Executors .newSingleThreadScheduledExecutor ();

Om de uitvoering van een enkele taak na een vaste vertraging te plannen, gebruikt u het gepland () methode van de ScheduledExecutorService. Er zijn er twee gepland () methoden waarmee u Runnable of Oproepbaar taken:

Toekomstig resultaatFuture = executorService.schedule (callableTask, 1, TimeUnit.SECONDS);

De scheduleAtFixedRate () methode laat toe een taak periodiek uit te voeren na een vaste vertraging. De bovenstaande code wordt een seconde vertraagd voordat deze wordt uitgevoerd callableTask.

Het volgende codeblok zal een taak uitvoeren na een aanvankelijke vertraging van 100 milliseconden, en daarna zal het elke 450 milliseconden dezelfde taak uitvoeren. Als de processor meer tijd nodig heeft om een ​​toegewezen taak uit te voeren dan het periode parameter van de scheduleAtFixedRate () methode, de ScheduledExecutorService wacht totdat de huidige taak is voltooid voordat de volgende wordt gestart:

Toekomstig resultaatFuture = service .scheduleAtFixedRate (runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Als het nodig is om een ​​vertraging met een vaste lengte tussen iteraties van de taak te hebben, scheduleWithFixedDelay () zou gebruikt moeten worden. De volgende code garandeert bijvoorbeeld een pauze van 150 milliseconden tussen het einde van de huidige uitvoering en het begin van een andere.

service.scheduleWithFixedDelay (taak, 100, 150, TimeUnit.MILLISECONDS);

Volgens de scheduleAtFixedRate () en scheduleWithFixedDelay () methode contracten eindigt de uitvoering van de taak bij het beëindigen van de ExecutorService of als er een uitzondering wordt gegenereerd tijdens de uitvoering van een taak.

7. ExecutorService vs. Fork / Join

Na de release van Java 7 hebben veel ontwikkelaars besloten dat het ExecutorService framework moet worden vervangen door het fork / join framework. Dit is echter niet altijd de juiste beslissing. Ondanks de eenvoud van gebruik en de frequente prestatieverbeteringen die gepaard gaan met fork / join, is er ook een vermindering van de hoeveelheid controle door de ontwikkelaar over gelijktijdige uitvoering.

ExecutorService geeft de ontwikkelaar de mogelijkheid om het aantal gegenereerde threads en de granulariteit van taken te beheren die door afzonderlijke threads moeten worden uitgevoerd. De beste use case voor ExecutorService is de verwerking van onafhankelijke taken, zoals transacties of verzoeken volgens het schema 'één thread voor één taak'.

Volgens de documentatie van Oracle is fork / join daarentegen ontworpen om het werk te versnellen dat recursief in kleinere stukjes kan worden opgesplitst.

8. Conclusie

Zelfs ondanks de relatieve eenvoud van ExecutorService, zijn er een paar veelvoorkomende valkuilen. Laten we ze samenvatten:

Een ongebruikt houden ExecutorService levend: In sectie 4 van dit artikel vindt u een gedetailleerde uitleg over het afsluiten van een ExecutorService;

Onjuiste thread-pool-capaciteit bij gebruik van thread-pool met vaste lengte: Het is erg belangrijk om te bepalen hoeveel threads de applicatie nodig heeft om taken efficiënt uit te voeren. Een te grote thread-pool zal onnodige overhead veroorzaken, alleen om threads te maken die meestal in de wachtmodus staan. Te weinig kunnen ervoor zorgen dat een applicatie niet meer reageert vanwege lange wachttijden voor taken in de wachtrij;

Een Toekomst‘S krijgen() methode na annulering van taak: Een poging om het resultaat te krijgen van een reeds geannuleerde taak zal een Annulering Uitzondering.

Onverwacht lang blokkeren met Toekomst‘S krijgen() methode: Time-outs moeten worden gebruikt om onverwachte wachttijden te voorkomen.

De code voor dit artikel is beschikbaar in een GitHub-opslagplaats.


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