1. CountDownLatch: Start per Signal
In der Welt der Nebenläufigkeit muss eine Gruppe von Threads oft reibungslos zusammenarbeiten – damit alle gemeinsam starten, fertig werden oder in die nächste Etappe übergehen. Zum Beispiel:
Stellen Sie sich ein Rennen vor. Die Autos stehen am Start – manche haben den Motor schon warmgelaufen, andere prüfen noch die Reifen. Aber solange der Schiedsrichter nicht die Flagge schwenkt, fährt niemand los. Genau das ist eine Koordinationsaufgabe.
Oder ein anderes Beispiel: Sie kochen mit Freunden – jemand schneidet Gemüse, jemand stellt Wasser auf, jemand sucht, wo das Salz geblieben ist. Wichtig ist, dass alle die Vorbereitung abgeschlossen haben, bevor mit dem Kochen begonnen wird.
Für solche Fälle gibt uns Java fertige Synchronisationswerkzeuge – sicher, verständlich und ohne den Schmerz von wait() und notify(). Eines der nützlichsten ist CountDownLatch. Es funktioniert wie ein Zählverschluss: Solange er nicht auf null fällt, ist die „Tür“ zu und niemand geht weiter. Sobald sich alle gemeldet haben, öffnet sich der Latch und die Threads stürmen synchron los.
CountDownLatch
CountDownLatch ist ein „Einwegventil“, das es einem oder mehreren Threads erlaubt zu warten, bis andere Threads eine bestimmte Anzahl von Operationen abgeschlossen haben.
Das ist wie der Start eines Marathons: Alle Läufer stehen an der Linie und warten auf den Schuss aus der Startpistole. Sobald der Schiedsrichter schießt (der Countdown erreicht null) – laufen alle los.
Wie das überhaupt funktioniert
CountDownLatch ist wie eine Startpfeife für Threads. Beim Erstellen geben Sie eine Zahl an – zum Beispiel 3. Das ist wie drei Signale, die empfangen werden müssen, bevor das Rennen beginnt.
Threads, die auf den Start warten sollen, rufen await() auf. Sie stehen an der Linie und sind startbereit, halten aber noch die Bremse gedrückt. Andere Threads rufen während der Vorbereitung nach und nach countDown() auf – als würden sie signalisieren: „Ich bin bereit!“
Sobald der Zähler die Null erreicht – peng! – starten alle wartenden Threads gleichzeitig.
Aber merken Sie sich: CountDownLatch ist einmalig. Nachdem der Zähler auf null gefallen ist, lässt er sich nicht zurücksetzen. Das ist kein Revolver, sondern ein Knaller: einmal peng – das war’s.
Beispiel: Auf Abschluss von N Aufgaben warten
import java.util.concurrent.CountDownLatch;
public class LatchDemo {
public static void main(String[] args) throws InterruptedException {
int workers = 3;
CountDownLatch latch = new CountDownLatch(workers);
for (int i = 1; i <= workers; i++) {
int id = i;
new Thread(() -> {
System.out.println("Arbeiter " + id + " hat die Arbeit begonnen");
try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
System.out.println("Arbeiter " + id + " hat die Arbeit beendet");
latch.countDown(); // Zähler verringern
}).start();
}
System.out.println("Der Hauptthread wartet auf den Abschluss aller Arbeiter...");
latch.await(); // warten, bis alle Arbeiter fertig sind
System.out.println("Alle Arbeiter sind fertig! Wir machen mit der Hauptarbeit weiter.");
}
}
Ausgabe:
Der Hauptthread wartet auf den Abschluss aller Arbeiter...
Arbeiter 1 hat die Arbeit begonnen
Arbeiter 2 hat die Arbeit begonnen
Arbeiter 3 hat die Arbeit begonnen
Arbeiter 1 hat die Arbeit beendet
Arbeiter 2 hat die Arbeit beendet
Arbeiter 3 hat die Arbeit beendet
Alle Arbeiter sind fertig! Wir machen mit der Hauptarbeit weiter.
Beispiel: Gleichzeitiger Start „per Signal“
CountDownLatch startSignal = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " wartet auf den Start");
startSignal.await(); // warten auf das Signal
System.out.println(Thread.currentThread().getName() + " startet!");
} catch (InterruptedException ignored) {}
}).start();
}
Thread.sleep(1000);
System.out.println("Startsignal!");
startSignal.countDown(); // alle Threads starten gleichzeitig
2. CyclicBarrier: mehrfache Phasen, Barriereaktionen
CyclicBarrier: Treffen am Lagerfeuer
CyclicBarrier ist ein Treffpunkt für Threads. Jeder läuft seine eigene Strecke, macht sein eigenes Ding, und dann treffen sich alle an der „Barriere“ – wie an einem Lagerfeuer in den Bergen. Wenn alle da sind, öffnet sich die Barriere und die Gruppe zieht gemeinsam weiter.
Der Hauptunterschied zu CountDownLatch – diese Barriere kann immer wieder verwendet werden. Nach jedem gemeinsamen Stopp „lädt“ sie sich neu, und das Team kann zur nächsten Etappe aufbrechen.
Stellen Sie sich vor: Eine Gruppe von Wanderern ist auf einer langen Route. Jeder bewegt sich in seinem Tempo: einer fotografiert Schmetterlinge, ein anderer sucht Wi‑Fi. Aber an jedem Pass treffen sie sich am Lagerfeuer, warten aufeinander und entscheiden, wohin es weitergeht. Das ist CyclicBarrier in Aktion.
Wie das funktioniert
Sie erstellen eine Barriere und geben an, wie viele Teilnehmer sich versammeln müssen, zum Beispiel 4. Jeder Thread ruft an der Kontrollstelle await() auf – und wartet auf die anderen. Wenn alle vier da sind, „klickt“ die Barriere und lässt alle weiter.
Sie können sogar eine „Barriereaktion“ angeben – ein Stück Code, das genau einmal ausgeführt wird, wenn die Gruppe zusammengekommen ist. Zum Beispiel das Lagerfeuer entzünden oder ein Log schreiben: „Etappe abgeschlossen, weiter geht’s“. Dafür übergibt man dem Konstruktor ein Runnable.
Wichtig: Anders als das einmalige CountDownLatch ist CyclicBarrier wiederverwendbar. Nach jedem „Treffen“ ist es wieder bereit für die nächste Etappe – wie ein ewiges Lagerfeuer, das man immer wieder entzünden kann.
Beispiel: Phasen synchronisieren
import java.util.concurrent.CyclicBarrier;
public class BarrierDemo {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("Alle sind an der Barriere angekommen! Neue Phase beginnt.");
});
for (int i = 1; i <= parties; i++) {
int id = i;
new Thread(() -> {
try {
System.out.println("Thread " + id + " arbeitet in Phase 1");
Thread.sleep(300 + id * 200);
System.out.println("Thread " + id + " wartet auf die Barriere");
barrier.await(); // auf die anderen warten
System.out.println("Thread " + id + " arbeitet in Phase 2");
Thread.sleep(200 + id * 100);
System.out.println("Thread " + id + " wartet auf die Barriere (2)");
barrier.await(); // wieder warten
System.out.println("Thread " + id + " hat die Arbeit beendet");
} catch (Exception e) {
System.out.println("Fehler: " + e);
}
}).start();
}
}
}
Ausgabe:
Thread 1 arbeitet in Phase 1
Thread 2 arbeitet in Phase 1
Thread 3 arbeitet in Phase 1
Thread 1 wartet auf die Barriere
Thread 2 wartet auf die Barriere
Thread 3 wartet auf die Barriere
Alle sind an der Barriere angekommen! Neue Phase beginnt.
Thread 1 arbeitet in Phase 2
...
Barriereaktion
Man kann dem Konstruktor von CyclicBarrier eine Aktion (Runnable) übergeben, die einmal ausgeführt wird, wenn alle Threads an der Barriere eingetroffen sind (z. B. Zustand aktualisieren, Log ausgeben).
Fallen: Was, wenn ein Thread abstürzt?
Wenn einer der Threads eine Ausnahme wirft oder die Barriere nicht erreicht, warten die anderen ewig – oder erhalten eine BrokenBarrierException. Die Barriere „bricht“ und muss neu erstellt werden.
So lässt sich dieser Abschnitt lebendiger, bildhafter und umgangssprachlicher formulieren – damit er sich wie eine natürliche Fortsetzung der „Orchester“-Linie liest:
3. Phaser: ein versierter Dirigent eines großen Konzerts
Phaser ist so etwas wie ein „Superbarriere“. Er vereint die besten Eigenschaften von CountDownLatch und CyclicBarrier und ist dabei viel flexibler. Das ist wie ein Orchester, in dem Musiker zwischen den Teilen des Konzerts kommen und gehen können, und der Dirigent sorgt dennoch dafür, dass jeder Teil beginnt, wenn alle bereit sind.
Im Unterschied zur gewöhnlichen Barriere arbeitet Phaser etappenweise – Phasen folgen aufeinander. Manche spielen nur im ersten Teil, andere steigen später ein, wieder andere gehen früher – all das steckt Phaser gelassen weg.
Wie das funktioniert
Zuerst wird ein Phaser erzeugt, meist mit einer vorgegebenen Anzahl von Teilnehmern – parties. Jeder Thread registriert sich (register()), führt seinen Part aus und ruft am Ende der Phase arriveAndAwaitAdvance() auf – meldet, dass er fertig ist, und wartet auf die anderen. Wenn alle diesen Punkt erreicht haben, schaltet der Phaser auf die nächste Phase um und der Prozess wiederholt sich.
Wenn ein Teilnehmer nicht mehr benötigt wird, kann er sich elegant „verabschieden“ und die Bühne über arriveAndDeregister() verlassen. Neue Teilnehmer können sich im Gegenzug sogar während des Konzerts anschließen – über register().
Wann Phaser besser ist als Barrier
Phaser sollten Sie wählen, wenn Ihr Programm nicht in einem, sondern in mehreren Takten lebt:
- die Anzahl der Threads sich zur Laufzeit ändert,
- es mehrere Etappen gibt und nicht alle Teilnehmer an allen teilnehmen müssen,
- oder wenn Sie einfach maximale Flexibilität ohne unnötige Mühe mit manueller Synchronisation wollen.
Im Grunde ist der Phaser ein Dirigent. Er schwingt nicht nur den Taktstock, sondern passt sich auch der Besetzung des Orchesters, der Anzahl der Konzertteile und sogar Verspätungen oder frühem Gehen an.
Beispiel: Etappenverarbeitung mit dynamischer Threadzahl
import java.util.concurrent.Phaser;
public class PhaserDemo {
public static void main(String[] args) {
Phaser phaser = new Phaser(1); // Hauptthread
for (int i = 1; i <= 3; i++) {
phaser.register(); // Teilnehmer registrieren
int id = i;
new Thread(() -> {
for (int phase = 1; phase <= 2; phase++) {
System.out.println("Thread " + id + " arbeitet in Phase " + phase);
try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
phaser.arriveAndAwaitAdvance(); // auf die anderen warten
}
System.out.println("Thread " + id + " hat die Arbeit beendet");
phaser.arriveAndDeregister(); // aus dem Phaser abmelden
}).start();
}
// Der Hauptthread nimmt ebenfalls an den Phasen teil
for (int phase = 1; phase <= 2; phase++) {
phaser.arriveAndAwaitAdvance();
System.out.println("Hauptthread: Phase " + phase + " abgeschlossen");
}
phaser.arriveAndDeregister();
System.out.println("Alle Phasen sind abgeschlossen!");
}
}
Besonderheiten:
- Teilnehmer können zur Laufzeit hinzugefügt/entfernt werden.
- Die aktuelle Phasennummer kann abgefragt werden: phaser.getPhase().
- Der Phaser kann beendet werden: phaser.forceTermination().
4. Exchanger: Austausch von Datenportionen zwischen Threads
Exchanger<T> ist ein Synchronisierer für den Datenaustausch zwischen zwei Threads. Jeder Thread ruft exchange(data) auf, und wenn beide Threads sich treffen, tauschen sie ihre Daten aus.
Analogie: Zwei Kuriere treffen sich an einer Kreuzung und tauschen Päckchen.
Wie funktioniert das?
- Ein Thread ruft exchange(data1) auf – wartet auf den zweiten.
- Der zweite Thread ruft exchange(data2) auf – beide erhalten die Daten des anderen.
- Kommt der zweite Thread nicht – wartet der erste (ein Timeout kann gesetzt werden).
Beispiel: Austausch von Puffern zwischen Producer und Consumer
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
// Producer
new Thread(() -> {
String data = "Daten vom Producer";
try {
System.out.println("Producer: sendet Daten");
String response = exchanger.exchange(data);
System.out.println("Producer: hat Antwort erhalten: " + response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Consumer
new Thread(() -> {
try {
String received = exchanger.exchange("Antwort vom Consumer");
System.out.println("Consumer: hat Daten erhalten: " + received);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Ausgabe:
Producer: sendet Daten
Consumer: hat Daten erhalten: Daten vom Producer
Producer: hat Antwort erhalten: Antwort vom Consumer
Anwendung:
- Austausch von Puffern zwischen Threads (z. B. einer liest aus einer Datei, ein anderer schreibt ins Netz).
- Phasensynchronisation zwischen zwei Threads.
5. Praxis: parallele Pipeline-Verarbeitung
Aufgabe: „Spiel‑Tick“ (Phasen)
Angenommen, wir haben mehrere Threads, von denen jeder für einen Teil der Spielwelt verantwortlich ist (z. B. Physik, KI, Rendering). Alle müssen sich bei jedem „Tick“ (Phase) synchronisieren, damit es keine Desynchronisation gibt.
Lösung: Verwenden Sie CyclicBarrier oder Phaser.
import java.util.concurrent.CyclicBarrier;
public class GameTickDemo {
public static void main(String[] args) {
int subsystems = 3;
CyclicBarrier barrier = new CyclicBarrier(subsystems, () -> {
System.out.println("Alle Subsysteme haben den Tick abgeschlossen. Nächster beginnt.");
});
for (int i = 1; i <= subsystems; i++) {
int id = i;
new Thread(() -> {
for (int tick = 1; tick <= 5; tick++) {
System.out.println("Subsystem " + id + " arbeitet im Tick " + tick);
try { Thread.sleep(100 + id * 50); } catch (InterruptedException ignored) {}
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
Aufgabe: „Ventil“ für eine große Zahl von Workern
Angenommen, wir haben 100 Worker-Threads, die nach der Vorbereitung gleichzeitig starten sollen (z. B. ein Lasttest).
Lösung: Verwenden Sie CountDownLatch.
import java.util.concurrent.CountDownLatch;
public class MassStartDemo {
public static void main(String[] args) throws InterruptedException {
int workers = 100;
CountDownLatch ready = new CountDownLatch(workers);
CountDownLatch start = new CountDownLatch(1);
for (int i = 0; i < workers; i++) {
new Thread(() -> {
System.out.println("Thread ist startbereit");
ready.countDown(); // Bereitschaft signalisieren
try {
start.await(); // auf das gemeinsame Signal warten
System.out.println("Thread startet!");
} catch (InterruptedException ignored) {}
}).start();
}
ready.await(); // warten, bis alle Threads bereit sind
System.out.println("Alle sind bereit! START!");
start.countDown(); // Startsignal geben
}
}
6. Typische Fehler beim Arbeiten mit Synchronisierern
Fehler Nr. 1: CountDownLatch als wiederverwendbare Barriere verwenden.
CountDownLatch ist einmalig! Nach Erreichen der Null lässt es sich nicht „neu laden“. Für wiederverwendbare Phasen nutzen Sie CyclicBarrier oder Phaser.
Fehler Nr. 2: Ausnahmen nicht behandelt (InterruptedException, BrokenBarrierException).
Methoden wie await() können Ausnahmen werfen – behandeln Sie sie immer, sonst kann der Thread „hängen bleiben“ oder mit einem Fehler enden. Achten Sie auf InterruptedException und BrokenBarrierException.
Fehler Nr. 3: Ein Thread hat die Barriere nicht erreicht.
Wenn ein Thread „abstürzt“ oder await() nicht aufruft, warten die anderen ewig (oder erhalten eine BrokenBarrierException). Achten Sie darauf, dass alle Teilnehmer die Barriere erreichen.
Fehler Nr. 4: deregister() im Phaser vergessen.
Wenn ein Thread seine Arbeit beendet, aber arriveAndDeregister() nicht aufruft, wartet der Phaser auf einen „toten“ Teilnehmer. Melden Sie Threads im Phaser immer korrekt ab.
Fehler Nr. 5: Exchanger für mehr als zwei Threads verwenden.
Exchanger funktioniert nur für den Austausch zwischen zwei Threads. Wenn es mehr Threads sind – droht Deadlock.
Fehler Nr. 6: Verschiedene Synchronisierer ohne Verständnis ihrer Funktionsweise mischen.
Verwenden Sie nicht gleichzeitig mehrere verschiedene Barrieren/Latches für dieselbe Threadgruppe – das kann zu Verwirrung und Hängern führen.
GO TO FULL VERSION