Interfejs Java ExecutorService, java.util.concurrent.ExecutorService, reprezentuje asynchroniczny mechanizm wykonawczy, który jest w stanie wykonywać zadania współbieżnie w tle. W tym poradniku Java ExecutorService wyjaśnię, jak utworzyć interfejs ExecutorService, jak przesyłać do niego zadania do wykonania, jak zobaczyć wyniki tych zadań i jak ponownie zamknąć ExecutorService, gdy zajdzie taka potrzeba.

Tutorial wideo Java ExecutorService

Jeśli wolisz wideo, mam wideo wprowadzenie do tego tutaj:

Java ExecutorService Tutorial Video - Part 1
Java ExecutorService Tutorial Video - Part 2

Delegacja zadania

Oto diagram ilustrujący wątek delegujący zadanie do Java ExecutorService do asynchronicznego wykonania:

Wątek delegujący zadanie do ExecutorService w celu asynchronicznego wykonania.

Wątek delegujący zadanie do usługi ExecutorService w celu asynchronicznego wykonania.

Po przekazaniu przez wątek zadania do usługi ExecutorService, wątek kontynuuje własne wykonanie niezależnie od wykonania tego zadania. The ExecutorService następnie wykonuje zadanie współbieżnie, niezależnie od wątku, który przekazał zadanie.

Przykład Java ExecutorService

Zanim zagłębimy się zbytnio w ExecutorService, spójrzmy na prosty przykład. Oto prosty przykład Java ExecutorService:

ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

Najpierw tworzony jest ExecutorService za pomocą metody fabrycznej ExecutorsnewFixedThreadPool(). Tworzy to pulę wątków z 10 wątkami wykonującymi zadania.

Po drugie, anonimowa implementacja interfejsu Runnable jest przekazywana do metody execute(). Powoduje to, że Runnable jest wykonywany przez jeden z wątków w ExecutorService.

Zobaczysz jeszcze kilka przykładów użycia ExecutorService w całym tym poradniku. Ten przykład ma za zadanie dać ci szybki przegląd tego, jak wygląda używanie ExecutorService do wykonywania zadań w tle.

Wdrożenia Java ExecutorService

Java ExecutorService jest bardzo podobna do puli wątków. W rzeczywistości, implementacja interfejsu ExecutorService obecna w pakiecie java.util.concurrent jest implementacją puli wątków. Jeśli chcesz zrozumieć, jak można wewnętrznie zaimplementować interfejs ExecutorService, przeczytaj powyższy tutorial.

Ponieważ ExecutorService jest interfejsem, potrzebujesz jego implementacji, aby w jakikolwiek sposób z niego skorzystać. Interfejs ExecutorService ma następującą implementację w pakiecie java.util.concurrent:

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

Tworzenie ExecutorService

To, w jaki sposób tworzysz ExecutorService, zależy od implementacji, której używasz. Jednakże, możesz użyć klasy fabrycznej Executors do tworzenia instancji ExecutorService również. Oto kilka przykładów tworzenia instancji ExecutorService:

ExecutorService executorService1 = Executors.newSingleThreadExecutor();ExecutorService executorService2 = Executors.newFixedThreadPool(10);ExecutorService executorService3 = Executors.newScheduledThreadPool(10);

Użycie usługi ExecutorService

Istnieje kilka różnych sposobów delegowania zadań do wykonania do ExecutorService:

  • execute(Runnable)
  • submit(Runnable)
  • submit(Callable)
  • invokeAny(….)
  • invokeAll(…)

Przyglądniemy się każdej z tych metod w kolejnych sekcjach.

Execute Runnable

Metoda Java ExecutorServiceexecute(Runnable) przyjmuje obiekt java.lang.Runnable i wykonuje go asynchronicznie. Oto przykład wykonania Runnable z ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

Nie ma sposobu na uzyskanie wyniku wykonanego Runnable, jeśli to konieczne. Będziesz musiał użyć do tego celu Callable (wyjaśnione w następnych sekcjach).

Submit Runnable

Metoda Java ExecutorServicesubmit(Runnable) również przyjmuje implementację Runnable, ale zwraca obiekt Future. Ten obiekt Future może być użyty do sprawdzenia, czy Runnable zakończył wykonywanie.

Oto przykład Java ExecutorServicesubmit():

Future future = executorService.submit(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});future.get(); //returns null if the task has finished correctly.

Metoda submit() zwraca obiekt Java Future, który może być użyty do sprawdzenia, kiedy Runnable zostało zakończone.

Submit Callable

Metoda Java ExecutorServicesubmit(Callable) jest podobna do metody submit(Runnable) z tym wyjątkiem, że przyjmuje Java Callable zamiast Runnable. Dokładna różnica między Callable a Runnable jest wyjaśniona nieco później.

Callable Wynik można uzyskać za pośrednictwem obiektu Java Future zwróconego przez metodę submit(Callable). Poniżej znajduje się przykład ExecutorServiceCallable:

Future future = executorService.submit(new Callable(){ public Object call() throws Exception { System.out.println("Asynchronous Callable"); return "Callable Result"; }});System.out.println("future.get() = " + future.get());

Powyższy przykład kodu wyświetli to:

Asynchronous Callablefuture.get() = Callable Result

invokeAny()

Metoda invokeAny() przyjmuje kolekcję obiektów Callable, lub podinterfejsów Callable. Wywołanie tej metody nie zwraca obiektu Future, ale zwraca wynik jednego z obiektów Callable. Nie masz żadnej gwarancji co do tego, który z wyników Callable’s otrzymasz. Po prostu jeden z tych, które się kończą.

Jeśli jedno Callable kończy się, tak że wynik jest zwracany z invokeAny(), to reszta instancji Callable jest anulowana.

Jeśli jedno z zadań się kończy (lub rzuca wyjątek), reszta Callable’s jest anulowana.

Oto przykład kodu:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});String result = executorService.invokeAny(callables);System.out.println("result = " + result);executorService.shutdown();

Ten przykład kodu wypisze obiekt zwrócony przez jeden z Callable’s w podanej kolekcji. Próbowałem uruchomić go kilka razy, a wynik się zmienia. Czasami jest to „Zadanie 1”, czasami „Zadanie 2” itd.

invokeAll()

Metoda invokeAll() wywołuje wszystkie obiekty Callable, które przekazujemy do niej w kolekcji przekazanej jako parametr. Metoda invokeAll() zwraca listę obiektów Future, za pomocą których można uzyskać wyniki wykonania każdego z Callable.

Należy pamiętać, że zadanie może zakończyć się z powodu wyjątku, więc może nie zakończyć się „sukcesem”. Nie ma sposobu, aby na Future odróżnić to od siebie.

Oto przykład kodu:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});List<Future<String>> futures = executorService.invokeAll(callables);for(Future<String> future : futures){ System.out.println("future.get = " + future.get());}executorService.shutdown();

Runnable vs. Callable

Interfejs Runnable jest bardzo podobny do interfejsu Callable. Interfejs Runnable reprezentuje zadanie, które może być wykonywane współbieżnie przez wątek lub ExecutorService. Callable może być wykonywany tylko przez ExecutorService. Oba interfejsy mają tylko jedną metodę. Jest jedna mała różnica między interfejsem Callable i Runnable chociaż. Różnica między interfejsem Runnable a Callable jest lepiej widoczna, gdy zobaczysz deklaracje interfejsu.

Oto najpierw deklaracja interfejsu Runnable:

public interface Runnable { public void run();}

A oto deklaracja interfejsu Callable:

public interface Callable{ public Object call() throws Exception;}

Główna różnica między metodą Runnablerun() a metodą Callablecall() jest to, że metoda call() może zwrócić Object z wywołania metody. Kolejną różnicą między call() a run() jest to, że call() może rzucić wyjątek, podczas gdy run() nie może (z wyjątkiem niezaznaczonych wyjątków – podklas RuntimeException).

Jeśli musisz przesłać zadanie do Java ExecutorService i potrzebujesz wyniku z zadania, to musisz sprawić, aby twoje zadanie implementowało interfejs Callable. W przeciwnym razie twoje zadanie może po prostu zaimplementować interfejs Runnable.

Anuluj zadanie

Możesz anulować zadanie (Runnable lub Callable) przekazane do interfejsu Java ExecutorService poprzez wywołanie metody cancel() na Future zwróconej po przesłaniu zadania. Anulowanie zadania jest możliwe tylko wtedy, gdy zadanie nie zostało jeszcze rozpoczęte. Oto przykład anulowania zadania poprzez wywołanie metody Future.cancel():

future.cancel();

Zamknięcie usługi ExecutorService

Po zakończeniu korzystania z usługi Java ExecutorService należy ją zamknąć, aby wątki nie były nadal uruchamiane. Jeśli twoja aplikacja jest uruchamiana za pomocą metody main() i twój główny wątek opuszcza twoją aplikację, aplikacja będzie nadal działać, jeśli masz aktywny ExexutorService w swojej aplikacji. Aktywne wątki wewnątrz tego ExecutorService uniemożliwiają wyłączenie maszyny JVM.

shutdown()

Aby zakończyć wątki wewnątrz ExecutorService wywołujesz jego metodę shutdown()ExecutorService nie zamknie się natychmiast, ale nie będzie już przyjmował nowych zadań, a gdy wszystkie wątki zakończą bieżące zadania, ExecutorService zamknie się. Wszystkie zadania przesłane do ExecutorService zanim shutdown() zostanie wywołany, są wykonywane. Oto przykład wykonania wyłączenia Java ExecutorService:

executorService.shutdown();

shutdownNow()

Jeśli chcesz natychmiast zamknąć ExecutorService, możesz wywołać metodę shutdownNow(). Spowoduje to próbę natychmiastowego zatrzymania wszystkich wykonywanych zadań i pominie wszystkie przesłane, ale nieprzetworzone zadania. Nie ma żadnych gwarancji co do wykonywanych zadań. Być może zatrzymają się, być może wykonają się do końca. Jest to próba najlepszego wysiłku. Oto przykład wywołania ExecutorServiceshutdownNow:

executorService.shutdownNow();

awaitTermination()

Metoda ExecutorServiceawaitTermination() zablokuje wywołujący ją wątek, dopóki albo ExecutorService nie wyłączy się całkowicie, lub do momentu wystąpienia określonego limitu czasu. Metoda awaitTermination() jest zazwyczaj wywoływana po wywołaniu shutdown() lub shutdownNow(). Oto przykład wywołania metody ExecutorServiceawaitTermination():

executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *