Die newFixedThreadPool- Methode der Executors- Klasse erstellt einen executorService mit einer festen Anzahl von Threads. Im Gegensatz zur newSingleThreadExecutor- Methode geben wir an, wie viele Threads wir im Pool haben möchten. Unter der Haube wird folgender Code aufgerufen:


new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

Die Parameter corePoolSize (die Anzahl der Threads, die bereit (gestartet) sind, wenn der Executor- Dienst startet) und MaximumPoolSize (die maximale Anzahl von Threads, die der Executor- Dienst erstellen kann) erhalten denselben Wert – die Anzahl der Threads, die an newFixedThreadPool(nThreads) übergeben werden ) . Und wir können unsere eigene Implementierung von ThreadFactory auf genau die gleiche Weise übergeben.

Mal sehen, warum wir so einen ExecutorService brauchen .

Hier ist die Logik eines ExecutorService mit einer festen Anzahl (n) von Threads:

  • Es sind maximal n Threads zur Bearbeitung von Aufgaben aktiv.
  • Wenn mehr als n Aufgaben übermittelt werden, werden diese in der Warteschlange gehalten, bis Threads frei werden.
  • Wenn einer der Threads ausfällt und beendet wird, wird an seiner Stelle ein neuer Thread erstellt.
  • Jeder Thread im Pool ist aktiv, bis der Pool heruntergefahren wird.

Stellen Sie sich zum Beispiel vor, Sie warten auf die Sicherheitskontrolle am Flughafen. Alle stehen in einer Schlange, bis unmittelbar vor der Sicherheitskontrolle die Passagiere auf alle funktionierenden Kontrollpunkte verteilt werden. Kommt es an einem der Kontrollpunkte zu einer Verzögerung, wird die Warteschlange nur vom zweiten bearbeitet, bis der erste frei ist. Und wenn ein Kontrollpunkt vollständig geschlossen wird, wird an seiner Stelle ein anderer Kontrollpunkt eröffnet, und die Passagiere werden weiterhin über zwei Kontrollpunkte abgefertigt.

Wir stellen gleich fest, dass selbst unter idealen Bedingungen – die versprochenen n Threads funktionieren stabil und Threads, die mit einem Fehler enden, immer ersetzt werden (was auf einem echten Flughafen aufgrund begrenzter Ressourcen nicht zu erreichen ist) – das System immer noch über mehrere Threads verfügt unangenehme Eigenschaften, denn auf keinen Fall wird es mehr Threads geben, auch wenn die Warteschlange schneller wächst, als die Threads Aufgaben bearbeiten können.

Ich schlage vor, sich ein praktisches Verständnis dafür zu verschaffen, wie ExecutorService mit einer festen Anzahl von Threads funktioniert. Erstellen wir eine Klasse, die Runnable implementiert . Objekte dieser Klasse repräsentieren unsere Aufgaben für den ExecutorService .


public class Task implements Runnable {
    int taskNumber;
 
    public Task(int taskNumber) {
        this.taskNumber = taskNumber;
    }
 
    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}
    

In der run()- Methode blockieren wir den Thread für 2 Sekunden, simulieren so eine gewisse Arbeitslast und zeigen dann die Nummer der aktuellen Aufgabe und den Namen des Threads an, der die Aufgabe ausführt.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }
        
        executorService.shutdown();
    

Zunächst erstellen wir in der Hauptmethode einen ExecutorService und übermitteln 30 Aufgaben zur Ausführung.

Verarbeitete Benutzeranforderung Nr. 1 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 0 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 2 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranforderung Nr. 5 im Thread „Pool-1-Thread-3“ 1-Thread-3-Thread
Verarbeitete Benutzeranforderung Nr. 3 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 4 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 8 im Thread „Pool-1-Thread-1“
Verarbeiteter Benutzer Anfrage Nr. 6 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranfrage Nr. 7 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranfrage Nr. 10 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranfrage Nr. 9 im Thread „Pool-1-Thread-3“ Thread-1-Thread
Verarbeitete Benutzeranforderung Nr. 11 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 12 im Thread „Pool-1-Thread-3“.
Verarbeitete Benutzeranforderung Nr. 14 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 13 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 15 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranforderung Nr. 16 im Thread „Pool-1-Thread-3“ 1-Thread-2-Thread
Verarbeitete Benutzeranforderung Nr. 17 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 18 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranforderung Nr. 19 im Thread „Pool-1-Thread-2“
Verarbeiteter Benutzer Anfrage Nr. 20 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranfrage Nr. 21 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranfrage Nr. 22 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranfrage Nr. 23 im Thread „Pool-1-Thread-2“ Thread-1-Thread
Verarbeitete Benutzeranforderung Nr. 25 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 24 im Thread „Pool-1-Thread-3“.
Verarbeitete Benutzeranforderung Nr. 26 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 27 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 28 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranforderung Nr. 29 im Thread „Pool-1-Thread-3“ 1-Thread-1-Thread

Die Konsolenausgabe zeigt uns, wie die Aufgaben auf verschiedenen Threads ausgeführt werden, nachdem sie von der vorherigen Aufgabe freigegeben wurden.

Jetzt erhöhen wir die Anzahl der Aufgaben auf 100 und rufen nach dem Absenden von 100 Aufgaben die Methode „awaitTermination(11, SECONDS)“ auf. Als Argumente übergeben wir eine Zahl und eine Zeiteinheit. Diese Methode blockiert den Hauptthread für 11 Sekunden. Dann rufen wir „shutdownNow()“ auf, um das Herunterfahren des ExecutorService zu erzwingen , ohne auf den Abschluss aller Aufgaben zu warten.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }
 
        executorService.awaitTermination(11, SECONDS);
 
        executorService.shutdownNow();
        System.out.println(executorService);
    

Am Ende zeigen wir Informationen über den Status des executorService an .

Hier ist die Konsolenausgabe, die wir erhalten:

Verarbeitete Benutzeranforderung Nr. 0 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 2 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranforderung Nr. 1 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 4 im Thread „Pool-1-Thread-2“ 1-Thread-3-Thread
Verarbeitete Benutzeranforderung Nr. 5 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 3 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 6 im Thread „Pool-1-Thread-3“
Verarbeiteter Benutzer Anfrage Nr. 7 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranfrage Nr. 8 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranfrage Nr. 9 im Thread „Pool-1-Thread-3“
Verarbeitete Benutzeranfrage Nr. 11 im Thread „Pool-1-Thread-3“ Thread-1-Thread
Verarbeitete Benutzeranforderung Nr. 10 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 13 im Thread „Pool-1-Thread-1“.
Verarbeitete Benutzeranforderung Nr. 14 im Thread „Pool-1-Thread-2“
Verarbeitete Benutzeranforderung Nr. 12 im Thread „Pool-1-Thread-3“
java.util.concurrent.ThreadPoolExecutor@452b3a41[Herunterfahren, Poolgröße = 3, aktive Threads = 3 , Aufgaben in der Warteschlange = 0, abgeschlossene Aufgaben = 15]
Verarbeitete Benutzeranforderung Nr. 17 im Thread „Pool-1-Thread-3“.
Verarbeitete Benutzeranforderung Nr. 15 im Thread „Pool-1-Thread-1“
Verarbeitete Benutzeranforderung Nr. 16 im Thread „Pool-1-Thread“. -2 Thread

Darauf folgen drei InterruptedExceptions , die von Schlafmethoden aus drei aktiven Aufgaben ausgelöst werden .

Wir können sehen, dass am Ende des Programms 15 Aufgaben erledigt sind, der Pool aber immer noch 3 aktive Threads hatte, die die Ausführung ihrer Aufgaben nicht abgeschlossen haben. Die Methode interrupt() wird in diesen drei Threads aufgerufen, was bedeutet, dass die Aufgabe abgeschlossen wird, aber in unserem Fall löst die Methode sleep eine InterruptedException aus . Wir sehen auch, dass die Aufgabenwarteschlange geleert wird, nachdem die Methode „shutdownNow()“ aufgerufen wurde.

Wenn Sie also einen ExecutorService mit einer festen Anzahl von Threads im Pool verwenden, denken Sie unbedingt daran, wie er funktioniert. Dieser Typ eignet sich für Aufgaben mit bekanntermaßen konstanter Belastung.

Hier ist eine weitere interessante Frage: Welche Methode sollten Sie aufrufen, wenn Sie einen Executor für einen einzelnen Thread verwenden müssen? newSingleThreadExecutor() oder newFixedThreadPool(1) ?

Beide Ausführenden verhalten sich gleichwertig. Der einzige Unterschied besteht darin, dass die Methode newSingleThreadExecutor() einen Executor zurückgibt, der später nicht für die Verwendung zusätzlicher Threads neu konfiguriert werden kann.