Ausführende newCachedThreadPool () vs newFixedThreadPool ()

1. Übersicht

Bei der Implementierung von Thread-Pools bietet die Java-Standardbibliothek zahlreiche Optionen zur Auswahl. Die festen und zwischengespeicherten Thread-Pools sind unter diesen Implementierungen ziemlich allgegenwärtig.

In diesem Tutorial werden wir sehen, wie Thread-Pools unter der Haube funktionieren, und dann diese Implementierungen und ihre Anwendungsfälle vergleichen.

2. Zwischengespeicherter Thread-Pool

Schauen wir uns an, wie Java einen zwischengespeicherten Thread-Pool erstellt, wenn wir Executors.newCachedThreadPool () aufrufen :

public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

Zwischengespeicherte Thread-Pools verwenden "synchrone Übergabe", um neue Aufgaben in die Warteschlange zu stellen. Die Grundidee der synchronen Übergabe ist einfach und dennoch kontraintuitiv: Man kann einen Artikel genau dann in die Warteschlange stellen, wenn ein anderer Thread diesen Artikel gleichzeitig nimmt. Mit anderen Worten, die SynchronousQueue kann keinerlei Aufgaben enthalten.

Angenommen, eine neue Aufgabe kommt herein. Wenn ein inaktiver Thread in der Warteschlange wartet, übergibt der Aufgabenproduzent die Aufgabe an diesen Thread. Andernfalls erstellt der Executor einen neuen Thread, um diese Aufgabe zu erledigen, da die Warteschlange immer voll ist .

Der zwischengespeicherte Pool beginnt mit null Threads und kann möglicherweise zu Integer.MAX_VALUE- Threads werden. Praktisch ist die einzige Einschränkung für einen zwischengespeicherten Thread-Pool die verfügbaren Systemressourcen.

Um die Systemressourcen besser zu verwalten, werden zwischengespeicherte Thread-Pools Threads entfernen, die eine Minute lang inaktiv bleiben.

2.1. Anwendungsfälle

Die zwischengespeicherte Thread-Pool-Konfiguration speichert die Threads (daher der Name) für kurze Zeit zwischen, um sie für andere Aufgaben wiederzuverwenden. Daher funktioniert es am besten, wenn wir eine angemessene Anzahl von kurzlebigen Aufgaben erledigen.

Der Schlüssel hier ist "vernünftig" und "kurzlebig". Um diesen Punkt zu verdeutlichen, bewerten wir ein Szenario, in dem zwischengespeicherte Pools nicht gut passen. Hier werden wir eine Million Aufgaben einreichen, deren Fertigstellung jeweils 100 Mikrosekunden dauert:

Callable task = () -> { long oneHundredMicroSeconds = 100_000; long startedAt = System.nanoTime(); while (System.nanoTime() - startedAt  task).collect(toList()); var result = cachedPool.invokeAll(tasks);

Dadurch werden viele Threads erstellt, die zu einer unangemessenen Speichernutzung führen, und noch schlimmer, viele CPU-Kontextwechsel. Diese beiden Anomalien würden die Gesamtleistung erheblich beeinträchtigen.

Daher sollten wir diesen Thread-Pool vermeiden, wenn die Ausführungszeit nicht vorhersehbar ist, wie z. B. E / A-gebundene Aufgaben.

3. Fadenpool behoben

Mal sehen, wie feste Thread-Pools unter der Haube funktionieren:

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

Im Gegensatz zum zwischengespeicherten Thread-Pool verwendet dieser eine unbegrenzte Warteschlange mit einer festen Anzahl von nie ablaufenden Threads . Daher versucht der feste Thread-Pool anstelle einer ständig wachsenden Anzahl von Threads, eingehende Aufgaben mit einer festen Anzahl von Threads auszuführen . Wenn alle Threads ausgelastet sind, stellt der Executor neue Aufgaben in die Warteschlange. Auf diese Weise haben wir mehr Kontrolle über den Ressourcenverbrauch unseres Programms.

Feste Thread-Pools eignen sich daher besser für Aufgaben mit unvorhersehbaren Ausführungszeiten.

4. Unglückliche Ähnlichkeiten

Bisher haben wir nur die Unterschiede zwischen zwischengespeicherten und festen Thread-Pools aufgelistet.

Abgesehen von all diesen Unterschieden verwenden beide AbortPolicy als Sättigungsrichtlinie . Wir erwarten daher, dass diese Executoren eine Ausnahme auslösen, wenn sie keine weiteren Aufgaben annehmen und sogar in die Warteschlange stellen können.

Mal sehen, was in der realen Welt passiert.

Zwischengespeicherte Thread-Pools erstellen unter extremen Umständen immer mehr Threads, sodass sie praktisch nie einen Sättigungspunkt erreichen . In ähnlicher Weise fügen feste Thread-Pools immer mehr Aufgaben in ihre Warteschlange ein. Daher erreichen die festen Pools auch niemals einen Sättigungspunkt .

Da beide Pools nicht gesättigt sind, verbrauchen sie bei außergewöhnlich hoher Auslastung viel Speicher für die Erstellung von Threads oder Warteschlangenaufgaben. Zwischengespeicherte Thread-Pools verursachen zusätzlich zu der Verletzung viele Prozessor-Kontextwechsel.

Um mehr Kontrolle über den Ressourcenverbrauch zu haben, wird dringend empfohlen, einen benutzerdefinierten ThreadPoolExecutor zu erstellen :

var boundedQueue = new ArrayBlockingQueue(1000); new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy()); 

Hier kann unser Thread-Pool bis zu 20 Threads haben und nur bis zu 1000 Aufgaben in die Warteschlange stellen. Wenn es keine Last mehr aufnehmen kann, wird einfach eine Ausnahme ausgelöst.

5. Schlussfolgerung

In diesem Tutorial haben wir einen Blick in den JDK-Quellcode geworfen, um zu sehen, wie verschiedene Executors unter der Haube funktionieren. Anschließend haben wir die festen und zwischengespeicherten Thread-Pools und ihre Anwendungsfälle verglichen.

Am Ende haben wir versucht, den außer Kontrolle geratenen Ressourcenverbrauch dieser Pools mit benutzerdefinierten Thread-Pools zu beheben.