1. Översikt
ExecutorService är ett ramverk som tillhandahålls av JDK som förenklar utförandet av uppgifter i asynkront läge. Generellt sett tillhandahåller ExecutorService automatiskt en pool av trådar och API för att tilldela uppgifter till den.
2. Installera ExecutorService
2.1. Fabriksmetoder i Executors Class
Det enklaste sättet att skapa ExecutorService är att använda en av fabriksmetoderna i Executors- klassen.
Till exempel kommer följande kodrad att skapa en trådpool med 10 trådar:
ExecutorService executor = Executors.newFixedThreadPool(10);
Det finns flera andra fabriksmetoder för att skapa fördefinierade ExecutorService som uppfyller specifika användningsfall. För att hitta den bästa metoden för dina behov, se Oracles officiella dokumentation.
2.2. Skapa direkt en ExecutorService
Eftersom ExecutorService är ett gränssnitt kan en instans av dess implementeringar användas. Det finns flera implementeringar att välja mellan i paketet java.util.concurrent eller så kan du skapa dina egna.
Till exempel har ThreadPoolExecutor- klassen några konstruktörer som kan användas för att konfigurera en exekveringstjänst och dess interna pool.
ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
Du kanske märker att koden ovan är mycket lik källkoden för fabriksmetoden newSingleThreadExecutor (). För de flesta fall är en detaljerad manuell konfiguration inte nödvändig.
3. Tilldela uppgifter till ExecutorService
ExecutorService kan utföra körbara och anropbara uppgifter. För att göra det enkelt i den här artikeln kommer två primitiva uppgifter att användas. Observera att lambdauttryck används här istället för anonyma inre klasser:
Runnable runnableTask = () -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } }; Callable callableTask = () -> { TimeUnit.MILLISECONDS.sleep(300); return "Task's execution"; }; List
callableTasks = new ArrayList(); callableTasks.add(callableTask); callableTasks.add(callableTask); callableTasks.add(callableTask);
Uppgifter kan tilldelas ExecutorService med flera metoder, inklusive execute () , som ärvs från Executor- gränssnittet, och även submit () , invokeAny (), invokeAll ().
Den execute () metoden är ogiltig, och det ger inte någon möjlighet att få resultatet av exekverings uppgift är eller för att kontrollera uppgiften status (det körs eller körs).
executorService.execute(runnableTask);
submit () skickar en anropbar eller en körbar uppgift till en ExecutorService och returnerar ett resultat av typen Future .
Future future = executorService.submit(callableTask);
invokeAny () tilldelar en samling uppgifter till en ExecutorService, vilket får var och en att köras, och returnerar resultatet av en lyckad körning av en uppgift (om det fanns en lyckad körning) .
String result = executorService.invokeAny(callableTasks);
invokeAll () tilldelar en samling uppgifter till en ExecutorService, vilket får var och en att köras, och returnerar resultatet av alla uppgiftskörningar i form av en lista med objekt av typen Future .
List
futures = executorService.invokeAll(callableTasks);
Nu, innan vi går vidare, måste ytterligare två saker diskuteras: att stänga av en ExecutorService och hantera framtida avkastningstyper.
4. Stänga av en ExecutorService
I allmänhet förstörs inte ExecutorService automatiskt när det inte finns någon uppgift att bearbeta. Det kommer att hålla sig vid liv och vänta på att nytt arbete ska göras.
I vissa fall är detta till stor hjälp; till exempel om en app behöver bearbeta uppgifter som visas på oregelbunden basis eller om mängden av dessa uppgifter inte är känd vid sammanställning.
Å andra sidan kan en app nå sitt slut, men den kommer inte att stoppas eftersom en väntande ExecutorService kommer att få JVM att fortsätta springa.
För att stänga av en ExecutorService ordentligt har vi API: erna för shutdown () och shutdownNow () .
Den avstängning () metoden inte orsakar omedelbar destruktion av ExecutorService. Det gör att ExecutorService slutar acceptera nya uppgifter och stängs av efter att alla löpande trådar avslutat sitt nuvarande arbete.
executorService.shutdown();
De shutdownNow () metoden försöker förstöra ExecutorService omedelbart, men det garanterar inte att alla de löpande trådarna stoppas samtidigt. Den här metoden returnerar en lista över uppgifter som väntar på att behandlas. Det är upp till utvecklaren att bestämma vad de ska göra med dessa uppgifter.
List notExecutedTasks = executorService.shutDownNow();
Ett bra sätt att stänga av ExecutorService (vilket också rekommenderas av Oracle) är att använda båda dessa metoder i kombination med metoden awaitTermination () . Med detta tillvägagångssätt kommer ExecutorService först att sluta ta nya uppgifter och sedan vänta upp till en viss tidsperiod tills alla uppgifter ska slutföras. Om den tiden löper ut avbryts körningen omedelbart:
executorService.shutdown(); try { if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); }
5. Framtidens gränssnitt
The submit() and invokeAll() methods return an object or a collection of objects of type Future, which allows us to get the result of a task's execution or to check the task's status (is it running or executed).
The Future interface provides a special blocking method get() which returns an actual result of the Callable task's execution or null in the case of Runnable task. Calling the get() method while the task is still running will cause execution to block until the task is properly executed and the result is available.
Future future = executorService.submit(callableTask); String result = null; try { result = future.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
With very long blocking caused by the get() method, an application's performance can degrade. If the resulting data is not crucial, it is possible to avoid such a problem by using timeouts:
String result = future.get(200, TimeUnit.MILLISECONDS);
If the execution period is longer than specified (in this case 200 milliseconds), a TimeoutException will be thrown.
The isDone() method can be used to check if the assigned task is already processed or not.
The Future interface also provides for the cancellation of task execution with the cancel() method, and to check the cancellation with isCancelled() method:
boolean canceled = future.cancel(true); boolean isCancelled = future.isCancelled();
6. The ScheduledExecutorService Interface
The ScheduledExecutorService runs tasks after some predefined delay and/or periodically. Once again, the best way to instantiate a ScheduledExecutorService is to use the factory methods of the Executors class.
For this section, a ScheduledExecutorService with one thread will be used:
ScheduledExecutorService executorService = Executors .newSingleThreadScheduledExecutor();
To schedule a single task's execution after a fixed delay, us the scheduled() method of the ScheduledExecutorService. There are two scheduled() methods that allow you to execute Runnable or Callable tasks:
Future resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
The scheduleAtFixedRate() method lets execute a task periodically after a fixed delay. The code above delays for one second before executing callableTask.
The following block of code will execute a task after an initial delay of 100 milliseconds, and after that, it will execute the same task every 450 milliseconds. If the processor needs more time to execute an assigned task than the period parameter of the scheduleAtFixedRate() method, the ScheduledExecutorService will wait until the current task is completed before starting the next:
Future resultFuture = service .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
If it is necessary to have a fixed length delay between iterations of the task, scheduleWithFixedDelay() should be used. For example, the following code will guarantee a 150-millisecond pause between the end of the current execution and the start of another one.
service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);
According to the scheduleAtFixedRate() and scheduleWithFixedDelay() method contracts, period execution of the task will end at the termination of the ExecutorService or if an exception is thrown during task execution.
7. ExecutorService vs. Fork/Join
After the release of Java 7, many developers decided that the ExecutorService framework should be replaced by the fork/join framework. This is not always the right decision, however. Despite the simplicity of usage and the frequent performance gains associated with fork/join, there is also a reduction in the amount of developer control over concurrent execution.
ExecutorService gives the developer the ability to control the number of generated threads and the granularity of tasks which should be executed by separate threads. The best use case for ExecutorService is the processing of independent tasks, such as transactions or requests according to the scheme “one thread for one task.”
In contrast, according to Oracle's documentation, fork/join was designed to speed up work which can be broken into smaller pieces recursively.
8. Conclusion
Even despite the relative simplicity of ExecutorService, there are a few common pitfalls. Let's summarize them:
Keeping an unused ExecutorService alive: There is a detailed explanation in section 4 of this article about how to shut down an ExecutorService;
Wrong thread-pool capacity while using fixed length thread-pool: It is very important to determine how many threads the application will need to execute tasks efficiently. A thread-pool that is too large will cause unnecessary overhead just to create threads which mostly will be in the waiting mode. Too few can make an application seem unresponsive because of long waiting periods for tasks in the queue;
Calling a Future‘s get() method after task cancellation: An attempt to get the result of an already canceled task will trigger a CancellationException.
Unexpectedly-long blocking with Future‘s get() method: Timeouts should be used to avoid unexpected waits.
Koden för den här artikeln finns i ett GitHub-arkiv.