Do czego służy interfejs Executora?

Przed Javą 5 trzeba było samodzielnie napisać cały kod w aplikacji, aby zarządzać wątkami. Ponadto tworzenie obiektunowy wątekjest operacją zasobochłonną i tworzenie za każdym razem nowego wątku dla „lekkich” zadań nie było racjonalne. A ponieważ absolutnie wszyscy twórcy aplikacji wielowątkowych napotkali ten problem, postanowili wykupić tę funkcjonalność jako framework Executor w Javie.

Jaka jest główna idea? To proste: zamiast tworzyć nowy wątek dla każdego nowego zadania, wątki są przechowywane w pewnego rodzaju „magazynie”, a kiedy pojawia się nowe zadanie, istniejący wątek jest brany, a nowy nie jest tworzony.

Głównymi interfejsami tego frameworka są Executor , ExecutorService i ScheduledExecutorService , z których każdy rozszerza funkcjonalność poprzedniego.

Interfejs Executor jest podstawowym interfejsem, który deklaruje pojedynczą metodę void execute(Runnable command) , która uruchamia zadanie opisane w obiekcie typu Runnable .

Interfejs ExecutorService jest już bardziej interesujący. Zawiera metody zarządzania zakończeniem pracy, a także metody, które mogą zwrócić jakiś wynik. Przyjrzyjmy się bliżej jego metodom:

metoda Opis
anuluj zamknięcie (); Wywołanie metody powoduje zatrzymanie usługi ExecutorService . Wszystkie zadania, które zostały już przekazane do realizacji zostaną zrealizowane, nowe zadania nie będą przyjmowane.
List<Runnable> shutdownNow();

Wywołanie metody powoduje zatrzymanie usługi ExecutorService . Wszystkie zadania, które zostały już przekazane do przetworzenia, otrzymają polecenie Thread.interrupt . Zadania w kolejce są zwracane jako lista w wyniku wywołania metody.

Metoda nie czeka na zakończenie wszystkich zadań, które są „w toku” w momencie wywołania metody.

Ostrzeżenie: wywołanie metody może spowodować wyciek zasobów.

wartość logiczna isShutdown(); Sprawdza, czy usługa ExecutorService jest zatrzymana .
wartość logiczna isTerminated(); Zwraca wartość true, jeśli wszystkie zadania zostały zakończone od czasu zatrzymania usługi ExecutorService . Do czasu wywołania metody shutdown() lub shutdownNow() zawsze będzie zwracana wartość false .
boolean awaitTermination(długi limit czasu, jednostka czasu) zgłasza wyjątek InterruptedException;

Blokuje wątek, z którego jest uruchamiany po wywołaniu metody shutdown() , dopóki nie zostanie spełniony jeden z następujących warunków:

  • wykonanie wszystkich zaplanowanych zadań;
  • przedział czasu określony jako miną parametry metody;
  • bieżący wątek zostanie zakończony.

Zwraca true , jeśli wszystkie zadania zostały zakończone, i false , jeśli limit czasu wystąpił wcześniej.

<T> Przyszłe<T> przesyłanie(Wywoływalne<T> zadanie);

Dodaje zadanie wywoływalne ExecutorService i zwraca obiekt, który implementuje interfejs Future .

<T> to typ wyniku przesłanego zadania.

<T> Przyszłe<T> prześlij(Zadanie możliwe do wykonania, wynik T);

Dodaje zadanie Runnable do usługi ExecutorService i zwraca obiekt, który implementuje interfejs Future .

Parametr wyniku T określa, co zwróci wywołanie metody get() odebranego obiektuprzyszły.

Future<?> upload(Zadanie możliwe do wykonania);

Dodaje zadanie Runnable do usługi ExecutorService i zwraca obiekt, który implementuje interfejs Future .

Jeśli wywołamy metodę get() na wynikowym obiekcie Future , otrzymamy wartość null.

<T> List<Future<T>> invokeAll(Collection<? rozszerza zadania Callable<T>>) zgłasza wyjątek InterruptedException;

Przekazuje usłudze ExecutorService listę zadań, które można wywołać . Zwraca listę kontraktów futures, z których ma zostać uzyskany wynik operacji. Ta lista jest zwracana, gdy wszystkie przesłane zadania zostały zakończone.

Jeśli kolekcja zadań zostanie zmodyfikowana podczas działania metody , wynik metody jest niezdefiniowany.

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> zadania, long timeout, TimeUnit unit) zgłasza wyjątek InterruptedException;

Przekazuje usłudze ExecutorService listę zadań, które można wywołać . Zwraca listę kontraktów futures, z których ma zostać uzyskany wynik operacji. Ta lista jest zwracana, gdy wszystkie przekazane zadania zakończą wykonywanie lub po upływie czasu w parametrach metody, w zależności od tego, co nastąpi wcześniej.

Jeśli przekroczenie limitu czasu nastąpi wcześniej, zaległe zadania anulują ich wykonanie.

Uwaga: anulowane zadanie może nie zatrzymać swojej pracy, ale kontynuować działanie (ten efekt uboczny zobaczymy na przykładzie).

Jeśli kolekcja zadań zostanie zmodyfikowana podczas działania metody , wynik metody jest niezdefiniowany.

<T> T invokeAny(Collection<? rozszerza zadania Callable<T>>) zgłasza wyjątek InterruptedException, ExecutionException;

Przekazuje usłudze ExecutorService listę zadań, które można wywołać . Zwraca wynik jednego z zadań, które zakończyły wykonywanie bez zgłaszania wyjątku (jeśli istnieje).

Jeśli kolekcja zadań zostanie zmodyfikowana podczas działania metody , wynik metody jest niezdefiniowany.

<T> T invokeAny(Collection<? rozszerza zadania Callable<T>>, długi limit czasu, jednostka czasu) zgłasza wyjątek InterruptedException, ExecutionException, TimeoutException;

Przekazuje usłudze ExecutorService listę zadań, które można wywołać . Zwraca wynik jednego z zadań, które zakończyły wykonywanie bez zgłoszenia wyjątku (jeśli istnieje) przed upływem czasu określonego w parametrach metody.

Jeśli kolekcja zadań zostanie zmodyfikowana podczas działania metody , wynik metody jest niezdefiniowany.

Spójrzmy na mały przykład pracy z ExecutorService .


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Wyjście programu:

gotowe
zrobione
mam futures
sen 1: sen przerwany
sen 1: sen przerwany
zrobione
zrobione
prawda
prawda

Każde zadanie trwa 5 sekund. Ponieważ stworzyliśmy pulę dla dwóch wątków, pierwsze dwa wiersze na wyjściu są logiczne i zrozumiałe.

6 sekund po uruchomieniu programu zostaje wyzwolony limit czasu metody invokeAll , a wynik jest zwracany jako lista Future . Widać to po linii wyjściowej „dostał futures” .

Po wykonaniu pierwszych dwóch zadań przystąpiono do pracy dwóch kolejnych. Ale ponieważ czas ustawiony w metodzie invokeAll przekroczył limit czasu, te dwa zadania nie miały czasu na wykonanie i wysłano do nich polecenie „anuluj” . Dlatego dane wyjściowe pokazują dwa wiersze „sleep 1: uśpienie przerwane” .

A potem widać jeszcze dwa napisy „gotowe” . Jest to demonstracja efektu ubocznego, o którym pisałem przy opisie metody invokeAll .

Ostatnie, piąte zadanie nawet się nie uruchomiło, więc nie widzimy nic na ten temat w wynikach programu.

Ostatnie dwa wiersze są wynikiem wywołania metod isShutdown i isTerminated .

W tym przykładzie interesujące jest również zobaczenie statusu zadań podczas debugowania po upływie limitu czasu (punkt przerwania w wierszu „executorService.shutdown();” ):

Widzimy, że dwa zadania zostały zakończone: „Ukończono normalnie” i trzy zadania „Anulowano” .

ScheduledExecutorService

Aby zakończyć opowieść o executorach, przyjrzyjmy się usłudze ScheduledExecutorService .

Ma 4 metody:

metoda Opis
public ScheduledFuture<?> harmonogram (polecenie Runnable, duże opóźnienie, jednostka czasu); Planuje uruchomienie danego zadania Runnable raz po czasie określonym jako parametr.
public <V> ScheduledFuture<V> harmonogram (wywoływalny<V> wywoływalny, duże opóźnienie, jednostka czasu); Planuje uruchomienie danego zadania Callable raz po czasie określonym jako parametr.
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); Planuje okresowe wykonywanie danego zadania, które zostanie wykonane po raz pierwszy po czasie initialDelay , a każde kolejne uruchomienie rozpocznie się po okresie.
public ScheduledFuture<?> ScheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); Planuje okresowe wykonywanie danego zadania, które najpierw wykona się po czasie initialDelay , a każde kolejne wykonanie rozpocznie się z opóźnieniem (między zakończeniem poprzedniego wykonania a rozpoczęciem bieżącego).