Warum benötigen Sie möglicherweise einen ExecutorService für einen Thread?

Sie können die Methode Executors.newSingleThreadExecutor verwenden, um einen ExecutorService mit einem Pool zu erstellen, der einen einzelnen Thread enthält. Die Logik des Pools ist wie folgt:

  • Der Dienst führt jeweils nur eine Aufgabe aus.
  • Wenn wir N Aufgaben zur Ausführung einreichen, werden alle N Aufgaben nacheinander vom einzelnen Thread ausgeführt.
  • Wenn der Thread unterbrochen wird, wird ein neuer Thread erstellt, um alle verbleibenden Aufgaben auszuführen.

Stellen wir uns eine Situation vor, in der unser Programm die folgende Funktionalität benötigt:

Wir müssen Benutzeranfragen innerhalb von 30 Sekunden bearbeiten, jedoch nicht mehr als eine Anfrage pro Zeiteinheit.

Wir erstellen eine Aufgabenklasse zur Bearbeitung einer Benutzeranfrage:


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());
   }
}
    

Die Klasse modelliert das Verhalten bei der Verarbeitung einer eingehenden Anfrage und zeigt deren Nummer an.

Als Nächstes erstellen wir in der Hauptmethode einen ExecutorService für einen Thread, den wir zur sequentiellen Verarbeitung eingehender Anforderungen verwenden. Da die Aufgabenbedingungen „innerhalb von 30 Sekunden“ vorschreiben, fügen wir eine Wartezeit von 30 Sekunden hinzu und stoppen dann den ExecutorService zwangsweise .


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();
}
    

Nach dem Start des Programms zeigt die Konsole Meldungen zur Anfragebearbeitung an:

Anfrage Nr. 0 auf Thread-ID=16
verarbeitet Anfrage Nr. 1 auf Thread-ID=16
verarbeitet Anfrage Nr. 2 auf Thread-ID=16 verarbeitet

Anfrage Nr. 29 auf Thread-ID=16 verarbeitet

Nachdem die Anforderungen 30 Sekunden lang verarbeitet wurden, ruft executorService die Methode „shutdownNow()“ auf , die die aktuelle Aufgabe (die gerade ausgeführt wird) stoppt und alle ausstehenden Aufgaben abbricht. Danach wird das Programm erfolgreich beendet.

Aber nicht immer ist alles so perfekt, denn in unserem Programm könnte es leicht passieren, dass eine der Aufgaben, die vom einzigen Thread unseres Pools übernommen werden, nicht richtig funktioniert und sogar unseren Thread beendet. Wir können diese Situation simulieren, um herauszufinden, wie executorService in diesem Fall mit einem einzelnen Thread funktioniert.

Dazu beenden wir während der Ausführung einer der Aufgaben unseren Thread mit der unsicheren und veralteten Methode Thread.currentThread().stop() . Wir tun dies absichtlich, um die Situation zu simulieren, in der eine der Aufgaben den Thread beendet.

Wir ändern die Ausführungsmethode in der Task- Klasse:


@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());
}
    

Wir unterbrechen Aufgabe Nr. 5.

Sehen wir uns an, wie die Ausgabe aussieht, wenn der Thread am Ende von Aufgabe Nr. 5 unterbrochen wird:

Verarbeitete Anfrage Nr. 0 auf Thread-ID=16
Verarbeitete Anfrage Nr. 1 auf Thread-ID=16
Verarbeitete Anfrage Nr. 2 auf Thread-ID
=16 Verarbeitete Anfrage Nr. 3 auf Thread-ID=16
Verarbeitete Anfrage Nr. 4 auf Thread-ID=16
Verarbeitete Anfrage Nr. 6 auf Thread-ID=17
Anfrage Nr. 7 auf Thread-ID=17 verarbeitet

Anfrage Nr. 29 auf Thread-ID=17 verarbeitet

Wir sehen, dass nach der Unterbrechung des Threads am Ende von Aufgabe 5 die Aufgaben in einem Thread mit der Kennung 17 ausgeführt werden, obwohl sie zuvor in dem Thread mit der Kennung 16 ausgeführt wurden. Und weil unser Pool eine hat Wenn Sie einen einzelnen Thread verwenden, kann dies nur eines bedeuten: executorService ersetzte den gestoppten Thread durch einen neuen und führte die Aufgaben weiter aus.

Daher sollten wir newSingleThreadExecutor mit einem Single-Thread-Pool verwenden , wenn wir Aufgaben nacheinander und jeweils nur einzeln verarbeiten möchten und die Verarbeitung von Aufgaben aus der Warteschlange unabhängig vom Abschluss der vorherigen Aufgabe fortsetzen möchten (z. B. der Fall, in dem eine unserer Aufgaben beendet den Thread).

ThreadFactory

Wenn es um das Erstellen und Neuerstellen von Threads geht, kommen wir nicht umhin, es zu erwähnenThreadFactory.

AThreadFactoryist ein Objekt, das bei Bedarf neue Threads erstellt.

Wir können unsere eigene Thread-Erstellungsfabrik erstellen und eine Instanz davon an die Methode Executors.newSingleThreadExecutor(ThreadFactory threadFactory) übergeben .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Wir überschreiben die Methode zum Erstellen eines neuen Threads, indem wir einen Thread-Namen an den Konstruktor übergeben.

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;
            }
        });
                    
Wir haben den Namen und die Priorität des erstellten Threads geändert.

Wir sehen also, dass wir zwei überladene Executors.newSingleThreadExecutor -Methoden haben. Eine ohne Parameter und eine zweite mit einem ThreadFactory- Parameter.

Mithilfe einer ThreadFactory können Sie die erstellten Threads nach Bedarf konfigurieren, indem Sie beispielsweise Prioritäten festlegen, Thread-Unterklassen verwenden, dem Thread einen UncaughtExceptionHandler hinzufügen usw.