Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Lernen Executoren und Thread-Pool | Hochrangige Synchronisationsmechanismen
Multithreading in Java

bookExecutoren und Thread-Pool

Wir haben bereits eine Vielzahl von Mechanismen zur Unterstützung von Multithreading untersucht, und Executors ist einer davon!

Was sind Executors und Thread-Pooling?

Executors ist ein Mechanismus, der Abstraktionen auf hoher Ebene für die Verwaltung von Threads bereitstellt. Er ermöglicht das Erstellen und Verwalten eines Thread-Pools, der aus einer Gruppe von vorgefertigten Threads besteht, die bereit sind, Aufgaben auszuführen. Anstatt für jede Aufgabe einen neuen Thread zu erstellen, werden Aufgaben an den Pool gesendet, wo ihre Ausführung auf die Threads verteilt wird.

Was genau ist also ein Thread-Pool? Es handelt sich um eine Sammlung von vorgefertigten Threads, die bereit sind, Aufgaben auszuführen. Durch die Verwendung eines Thread-Pools wird der Overhead des ständigen Erstellens und Entfernens von Threads vermieden, da dieselben Threads für mehrere Aufgaben wiederverwendet werden können.

Note
Hinweis

Wenn es mehr Aufgaben als Threads gibt, warten die Aufgaben in der Task Queue. Eine Aufgabe aus der Warteschlange wird von einem verfügbaren Thread aus dem Pool übernommen, und sobald die Aufgabe abgeschlossen ist, übernimmt der Thread eine neue Aufgabe aus der Warteschlange. Sobald alle Aufgaben in der Warteschlange erledigt sind, bleiben die Threads aktiv und warten auf neue Aufgaben.

Beispiel aus dem Alltag

Stellen Sie sich ein Restaurant vor, in dem Köche (Threads) Bestellungen (Aufgaben) zubereiten. Anstatt für jede Bestellung einen neuen Koch einzustellen, beschäftigt das Restaurant eine begrenzte Anzahl an Köchen, die die Bestellungen nacheinander bearbeiten. Sobald ein Koch eine Bestellung abgeschlossen hat, übernimmt er die nächste, was eine effiziente Nutzung der Ressourcen des Restaurants ermöglicht.

Hauptmethode

newFixedThreadPool(int n): Erstellt einen Pool mit einer festen Anzahl von Threads, die n entspricht.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newFixedThreadPool(20);

newCachedThreadPool(): Erstellt einen Pool, der bei Bedarf neue Threads erzeugen kann, aber vorhandene Threads wiederverwendet, sofern verfügbar.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newCachedThreadPool();

newSingleThreadExecutor(): Erstellt einen Single-Thread-Pool, der sicherstellt, dass Aufgaben sequenziell ausgeführt werden, also nacheinander. Geeignet für Aufgaben, die in strikter Reihenfolge ausgeführt werden müssen.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newSingleThreadExecutor();

In allen Beispielen geben die Methoden von Executors eine Implementierung des ExecutorService-Interfaces zurück, die zur Verwaltung von Threads verwendet wird.

ExecutorService stellt Methoden zur Verwaltung eines Thread-Pools bereit. Zum Beispiel akzeptiert submit(Runnable task) eine Aufgabe als Runnable-Objekt und platziert sie in eine Warteschlange zur Ausführung. Es gibt ein Future-Objekt zurück, mit dem der Status der Aufgabe überprüft und ein Ergebnis abgerufen werden kann, falls die Aufgabe ein Ergebnis liefert.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031323334
package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }

Die Methode shutdown() startet ein geordnetes Herunterfahren des Thread-Pools. Sie akzeptiert keine neuen Aufgaben mehr, führt aber die aktuellen Aufgaben zu Ende. Nach dem Aufruf dieser Methode kann der Pool nicht neu gestartet werden.

Die Methode awaitTermination(long timeout, TimeUnit unit) wartet darauf, dass alle Aufgaben im Pool innerhalb des angegebenen Zeitrahmens abgeschlossen werden. Dies ist ein blockierendes Warten, das sicherstellt, dass alle Aufgaben vor dem endgültigen Abschluss des Pools beendet sind.

Außerdem wurde das wichtigste Interface zur Überwachung des Thread-Zustands noch nicht erwähnt: das Future-Interface. Die submit()-Methode des ExecutorService-Interfaces gibt eine Implementierung des Future-Interfaces zurück.

Um das Ergebnis der Thread-Ausführung zu erhalten, kann die Methode get() verwendet werden. Wenn der Thread Runnable implementiert, liefert die Methode get() keinen Wert zurück. Bei Callable<T> hingegen wird ein Wert vom Typ T zurückgegeben.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031
package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }

Mit der Methode cancel(boolean mayInterruptIfRunning) kann versucht werden, die Ausführung einer Aufgabe zu abbrechen. Falls die Aufgabe noch nicht gestartet wurde, wird sie abgebrochen. Läuft die Aufgabe bereits, kann sie je nach Wert des Flags mayInterruptIfRunning unterbrochen werden.

true: Falls die Aufgabe läuft, wird sie durch Aufruf von Thread.interrupt() im ausführenden Thread unterbrochen. false: Falls die Aufgabe läuft, wird sie nicht unterbrochen und der Abbruchversuch hat keine Auswirkung auf die aktuell laufende Aufgabe.

Sowie 2 Methoden, deren Funktion sich intuitiv erschließt:

  • isCancelled(): Prüft, ob die Aufgabe abgebrochen wurde;
  • isDone(): Prüft, ob die Aufgabe abgeschlossen wurde.

Anwendungsbeispiel

Note
Hinweis

Die Größe des Threadpools hängt von der Art der auszuführenden Aufgaben ab. In der Regel sollte die Größe des Threadpools nicht fest codiert werden, sondern anpassbar sein. Die optimale Größe wird durch Überwachung des Durchsatzes der ausgeführten Aufgaben bestimmt.

Es ist maximal effizient, die Anzahl der threads = processor cores zu verwenden. Dies kann im Code mit Runtime.getRuntime().availableProcessors() eingesehen werden.

Main.java

Main.java

copy
1
int availableProcessors = Runtime.getRuntime().availableProcessors();

Unterschiede zwischen direkter Thread-Erstellung und der Verwendung von ExecutorService

Die Hauptunterschiede zwischen der direkten Erstellung von Threads und der Verwendung von ExecutorService liegen in der Bequemlichkeit und dem Ressourcenmanagement. Die manuelle Erstellung von Threads erfordert die individuelle Verwaltung jedes Threads, was den Code und die Administration erschwert.

ExecutorService vereinfacht die Verwaltung durch die Nutzung eines Thread-Pools, wodurch die Aufgabenbearbeitung erleichtert wird. Während die manuelle Thread-Erstellung zu einem hohen Ressourcenverbrauch führen kann, ermöglicht ExecutorService die Anpassung der Größe des Thread-Pools.

War alles klar?

Wie können wir es verbessern?

Danke für Ihr Feedback!

Abschnitt 3. Kapitel 6

Fragen Sie AI

expand

Fragen Sie AI

ChatGPT

Fragen Sie alles oder probieren Sie eine der vorgeschlagenen Fragen, um unser Gespräch zu beginnen

Awesome!

Completion rate improved to 3.33

bookExecutoren und Thread-Pool

Swipe um das Menü anzuzeigen

Wir haben bereits eine Vielzahl von Mechanismen zur Unterstützung von Multithreading untersucht, und Executors ist einer davon!

Was sind Executors und Thread-Pooling?

Executors ist ein Mechanismus, der Abstraktionen auf hoher Ebene für die Verwaltung von Threads bereitstellt. Er ermöglicht das Erstellen und Verwalten eines Thread-Pools, der aus einer Gruppe von vorgefertigten Threads besteht, die bereit sind, Aufgaben auszuführen. Anstatt für jede Aufgabe einen neuen Thread zu erstellen, werden Aufgaben an den Pool gesendet, wo ihre Ausführung auf die Threads verteilt wird.

Was genau ist also ein Thread-Pool? Es handelt sich um eine Sammlung von vorgefertigten Threads, die bereit sind, Aufgaben auszuführen. Durch die Verwendung eines Thread-Pools wird der Overhead des ständigen Erstellens und Entfernens von Threads vermieden, da dieselben Threads für mehrere Aufgaben wiederverwendet werden können.

Note
Hinweis

Wenn es mehr Aufgaben als Threads gibt, warten die Aufgaben in der Task Queue. Eine Aufgabe aus der Warteschlange wird von einem verfügbaren Thread aus dem Pool übernommen, und sobald die Aufgabe abgeschlossen ist, übernimmt der Thread eine neue Aufgabe aus der Warteschlange. Sobald alle Aufgaben in der Warteschlange erledigt sind, bleiben die Threads aktiv und warten auf neue Aufgaben.

Beispiel aus dem Alltag

Stellen Sie sich ein Restaurant vor, in dem Köche (Threads) Bestellungen (Aufgaben) zubereiten. Anstatt für jede Bestellung einen neuen Koch einzustellen, beschäftigt das Restaurant eine begrenzte Anzahl an Köchen, die die Bestellungen nacheinander bearbeiten. Sobald ein Koch eine Bestellung abgeschlossen hat, übernimmt er die nächste, was eine effiziente Nutzung der Ressourcen des Restaurants ermöglicht.

Hauptmethode

newFixedThreadPool(int n): Erstellt einen Pool mit einer festen Anzahl von Threads, die n entspricht.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newFixedThreadPool(20);

newCachedThreadPool(): Erstellt einen Pool, der bei Bedarf neue Threads erzeugen kann, aber vorhandene Threads wiederverwendet, sofern verfügbar.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newCachedThreadPool();

newSingleThreadExecutor(): Erstellt einen Single-Thread-Pool, der sicherstellt, dass Aufgaben sequenziell ausgeführt werden, also nacheinander. Geeignet für Aufgaben, die in strikter Reihenfolge ausgeführt werden müssen.

Main.java

Main.java

copy
1
ExecutorService executorService = Executors.newSingleThreadExecutor();

In allen Beispielen geben die Methoden von Executors eine Implementierung des ExecutorService-Interfaces zurück, die zur Verwaltung von Threads verwendet wird.

ExecutorService stellt Methoden zur Verwaltung eines Thread-Pools bereit. Zum Beispiel akzeptiert submit(Runnable task) eine Aufgabe als Runnable-Objekt und platziert sie in eine Warteschlange zur Ausführung. Es gibt ein Future-Objekt zurück, mit dem der Status der Aufgabe überprüft und ein Ergebnis abgerufen werden kann, falls die Aufgabe ein Ergebnis liefert.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031323334
package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }

Die Methode shutdown() startet ein geordnetes Herunterfahren des Thread-Pools. Sie akzeptiert keine neuen Aufgaben mehr, führt aber die aktuellen Aufgaben zu Ende. Nach dem Aufruf dieser Methode kann der Pool nicht neu gestartet werden.

Die Methode awaitTermination(long timeout, TimeUnit unit) wartet darauf, dass alle Aufgaben im Pool innerhalb des angegebenen Zeitrahmens abgeschlossen werden. Dies ist ein blockierendes Warten, das sicherstellt, dass alle Aufgaben vor dem endgültigen Abschluss des Pools beendet sind.

Außerdem wurde das wichtigste Interface zur Überwachung des Thread-Zustands noch nicht erwähnt: das Future-Interface. Die submit()-Methode des ExecutorService-Interfaces gibt eine Implementierung des Future-Interfaces zurück.

Um das Ergebnis der Thread-Ausführung zu erhalten, kann die Methode get() verwendet werden. Wenn der Thread Runnable implementiert, liefert die Methode get() keinen Wert zurück. Bei Callable<T> hingegen wird ein Wert vom Typ T zurückgegeben.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031
package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }

Mit der Methode cancel(boolean mayInterruptIfRunning) kann versucht werden, die Ausführung einer Aufgabe zu abbrechen. Falls die Aufgabe noch nicht gestartet wurde, wird sie abgebrochen. Läuft die Aufgabe bereits, kann sie je nach Wert des Flags mayInterruptIfRunning unterbrochen werden.

true: Falls die Aufgabe läuft, wird sie durch Aufruf von Thread.interrupt() im ausführenden Thread unterbrochen. false: Falls die Aufgabe läuft, wird sie nicht unterbrochen und der Abbruchversuch hat keine Auswirkung auf die aktuell laufende Aufgabe.

Sowie 2 Methoden, deren Funktion sich intuitiv erschließt:

  • isCancelled(): Prüft, ob die Aufgabe abgebrochen wurde;
  • isDone(): Prüft, ob die Aufgabe abgeschlossen wurde.

Anwendungsbeispiel

Note
Hinweis

Die Größe des Threadpools hängt von der Art der auszuführenden Aufgaben ab. In der Regel sollte die Größe des Threadpools nicht fest codiert werden, sondern anpassbar sein. Die optimale Größe wird durch Überwachung des Durchsatzes der ausgeführten Aufgaben bestimmt.

Es ist maximal effizient, die Anzahl der threads = processor cores zu verwenden. Dies kann im Code mit Runtime.getRuntime().availableProcessors() eingesehen werden.

Main.java

Main.java

copy
1
int availableProcessors = Runtime.getRuntime().availableProcessors();

Unterschiede zwischen direkter Thread-Erstellung und der Verwendung von ExecutorService

Die Hauptunterschiede zwischen der direkten Erstellung von Threads und der Verwendung von ExecutorService liegen in der Bequemlichkeit und dem Ressourcenmanagement. Die manuelle Erstellung von Threads erfordert die individuelle Verwaltung jedes Threads, was den Code und die Administration erschwert.

ExecutorService vereinfacht die Verwaltung durch die Nutzung eines Thread-Pools, wodurch die Aufgabenbearbeitung erleichtert wird. Während die manuelle Thread-Erstellung zu einem hohen Ressourcenverbrauch führen kann, ermöglicht ExecutorService die Anpassung der Größe des Thread-Pools.

War alles klar?

Wie können wir es verbessern?

Danke für Ihr Feedback!

Abschnitt 3. Kapitel 6
some-alt