Dlaczego możesz potrzebować usługi ExecutorService dla 1 wątku?

Za pomocą metody Executors.newSingleThreadExecutor można utworzyć obiekt ExecutorService z pulą zawierającą pojedynczy wątek. Logika takiej puli jest następująca:

  • Usługa wykonuje tylko jedno zadanie naraz.
  • Jeśli wyślemy N zadań do wykonania, wszystkie N zadań jedno po drugim zostaną wykonane przez jeden wątek.
  • Jeśli wątek zostanie przerwany, zostanie utworzony nowy wątek do wykonania pozostałych zadań.

Wyobraźmy sobie sytuację, w której nasz program wymaga następującej funkcjonalności:

Żądania od użytkowników musimy przetwarzać w ciągu 30 sekund, ale nie więcej niż jedno żądanie na jednostkę czasu.

Utwórz klasę zadania do przetwarzania żądania od użytkowników:


class Task implements Runnable {
   private final int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

Klasa modeluje zachowanie przetwarzania przychodzącego żądania i wyświetla jego numer.

Następnie w metodzie main tworzymy ExecutorService dla 1 wątku, w którym sekwencyjnie przetwarzamy przychodzące żądania. Ponieważ warunek wskazany „w ciągu 30 sekund”, dodajemy oczekiwanie na zakończenie 30 sekund, po czym wymuszamy zatrzymanie usługi ExecutorService .


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

Podczas uruchamiania widzimy komunikaty przetwarzania danych wyjściowych w konsoli:

Przetworzone żądanie nr 0 w wątku id=16
Przetworzone żądanie nr 1 w wątku id=16
Przetworzone żądanie nr 2 w wątku id=16
….
Przetworzone żądanie nr 29 w wątku id=16

Po przetworzeniu żądań przez 30 sekund usługa executorService wywoła metodę shutdownNow() , która zatrzyma bieżące zadanie (które jest uruchomione) i anuluje wszystkie oczekujące zadania. Następnie program pomyślnie kończy swoje wykonanie.

Ale nie zawsze wszystko jest takie idealne, ponieważ w pracy programu łatwo może dojść do sytuacji, w której jedno z zadań, które trafia do naszego jedynego wątku w puli, nie zadziała poprawnie, a nawet zakończy nasz wątek. Możemy zasymulować taką sytuację, aby dowiedzieć się, jak executorService z pojedynczym wątkiem będzie działać w tym przypadku.

W tym celu na etapie wykonywania jednego z zadań kończymy nasz wątek za pomocą niebezpiecznej i przestarzałej metody Thread.currentThread().stop() . Robimy to celowo, aby zasymulować sytuację, w której wątek kończy się na jednym z zadań.

Zmieniamy naszą metodę uruchamiania w klasie Task :


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

Przerwiemy przy zadaniu nr 5.

Zobaczmy, jak wygląda wynik z przerwaniem wątku na końcu zadania nr 5:

Przetworzone żądanie nr 0 w id wątku = 16
Przetworzone żądanie nr 1 w id wątku = 16
Przetworzone żądanie nr 2 w id wątku = 16
Przetworzone żądanie nr 3 w id wątku = 16 Przetworzone żądanie nr
4 w id wątku = 16
Przetworzone żądanie nr 6 w id wątku=17
Przetworzone żądanie nr 7 w id wątku=17

Przetworzone żądanie nr 29 w id wątku=17

Widzimy, że po przerwaniu wątku na końcu zadania 5 zadania zaczynają być wykonywane w wątku o id = 17, chociaż przed tym zadaniem były wykonywane w wątku o id = 16. A ponieważ jest tylko jeden wątku w naszej puli, to może oznaczać tylko jedno - executorService zastąpił zatrzymany wątek nowym i zadania działały dalej.

Zatem użycie newSingleThreadExecutor do pracy z pulą z pojedynczym wątkiem przetwarzania zadań jest konieczne dla zadań sekwencyjnych, które implikują wykonanie tylko jednego zadania na raz, ale jednocześnie wymagają kontynuacji zadań przetwarzania z kolejki , pomimo wyniku wykonania zadania (przypadek, gdy w jednym z zadań wątek może zostać zabity).

Fabryka wątków

Mówiąc o tworzeniu i odtwarzaniu wątków, nie możemy nie wspomniećFabryka wątków.

Fabryka wątkówjest obiektem, który tworzy nowe wątki na żądanie.

Możemy stworzyć własną fabrykę tworzenia wątków i przekazać jej instancję do metody Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Zastąp metodę tworzenia nowego wątku, przekazując nazwę do konstruktora.

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
Zmieniono nazwę i priorytet utworzonego wątku.

W ten sposób dowiedzieliśmy się, że istnieją 2 przeciążone metody Executors.newSingleThreadExecutor . Jeden - bez parametrów, drugi - z parametrem typu ThreadFactory .

Używając ThreadFactory , możesz wprowadzić różne ustawienia dla tworzonych wątków, na przykład ustawić priorytety, użyć podklas wątków, dodać wątek UncaughtExceptionHandler i tak dalej.