1. Das Schlüsselwort synchronized: wozu und wie
In Java ist das Schlüsselwort synchronized wie ein Schild „Belegt!“ an der Toilettentür: Solange ein Thread sich innerhalb des „kritischen Abschnitts“ befindet, warten die anderen höflich auf ihre Reihe. Erst wenn der erste herauskommt, darf der nächste eintreten und seinen Code ausführen.
Syntax: Block und Methode
Synchronisierter Block
synchronized (object) {
// kritischer Abschnitt
}
- object – ist ein beliebiges Objekt, an dem Sie das „Schloss“ anbringen möchten. Solange ein Thread diesen Block ausführt, müssen andere Threads, die ebenfalls in einen Block mit demselben Objekt eintreten wollen, warten.
Synchronisierte Methode
public synchronized void increment() {
// kritischer Abschnitt
}
- Hier wird das „Schloss“ am Objekt selbst angebracht (this). Das heißt, nur ein Thread nach dem anderen kann irgendeine synchronisierte Methode dieses Objekts ausführen.
Statische synchronisierte Methode
public static synchronized void foo() {
// kritischer Abschnitt
}
- Hier erfolgt die Sperre auf Klassenebene (ClassName.class), nicht auf einem konkreten Objekt.
Wie funktioniert das unter der Haube
Wenn ein Thread in einen synchronisierten Block oder eine synchronisierte Methode eintritt, erwirbt er den „Monitor“ des Objekts (oder der Klasse bei statischen Methoden). Ist der Monitor bereits belegt – wartet der Thread. Sobald der Monitor freigegeben wird, kann der nächste Thread eintreten.
2. Beispiel: Counter-Inkrement mit und ohne Synchronisierung
Ohne Synchronisierung
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class CounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Endwert: " + counter.getCount());
}
}
Erwarteter Wert: 2000
Tatsächlicher Wert: kann geringer sein (z. B. 1995, 1987...), und bei jedem Start – eine eigene „Überraschung“.
Warum? Weil die Operation count++ nicht atomar ist: Sie wird in drei Schritte zerlegt – lesen, erhöhen, zurückschreiben. Wenn zwei Threads das gleichzeitig tun, können sie das Ergebnis des jeweils anderen „überschreiben“.
Lösung: synchronized
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Jetzt kann immer nur ein Thread zurzeit die Methode increment() ausführen. Der Endwert wird stets 2000 sein.
Alternative: synchronisierter Block
public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
Das Ergebnis ist dasselbe. Man kann nicht die gesamte Methode, sondern nur den benötigten Teil synchronisieren.
3. Einführung in den „Objektmonitor“
Ein Monitor ist ein „Schloss“, das in jedes Objekt in Java eingebaut ist. Wenn Sie synchronized(object) schreiben, versucht der Thread, dieses Objekt zu „verriegeln“. Ist das Schloss frei – erhält der Thread es; wenn nicht – wartet er auf seine Reihe. Sobald der Thread den Block verlässt, wird das Schloss freigegeben.
Wichtig! Wenn Sie auf unterschiedlichen Objekten synchronisieren, werden Threads nicht aufeinander warten. Daher ist es sehr wichtig, das richtige Objekt für die Synchronisierung zu wählen.
Statische synchronisierte Methoden
Manchmal ist die gemeinsame Ressource nicht ein Objekt, sondern etwas, das für alle Instanzen der Klasse gemeinsam ist (z. B. eine statische Variable). In diesem Fall muss die Synchronisierung auf Klassenebene erfolgen.
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
Das ist gleichbedeutend mit:
public static void increment() {
synchronized (StaticCounter.class) {
count++;
}
}
Der Monitor hängt am Klassenobjekt (Class), nicht an einer konkreten Instanz.
4. Schlüsselwort volatile: was ist das und wozu
Sichtbarkeitsproblem zwischen Threads
In Java kann jeder Thread zur Beschleunigung Werte von Variablen zwischenspeichern. Das bedeutet: Wenn ein Thread eine Variable ändert, kann ein anderer Thread das „nicht bemerken“ und weiterhin den Wert aus seinem lokalen Cache lesen. Das ist besonders kritisch bei Flags, mit denen Threads einander Signale geben.
Wie volatile funktioniert
Wenn eine Variable als volatile deklariert ist, bedeutet das:
- Alle Threads lesen und schreiben sie immer im Hauptspeicher, unter Umgehung des Caches.
- Jede Änderung der Variable wird sofort für alle Threads sichtbar.
Aber! Operationen mit volatile sind für sich genommen nicht atomar (außer einfachem Lesen/Schreiben von Primitiven wie boolean, int usw.). Wenn Sie etwas Komplexeres als eine Zuweisung machen – ist Synchronisierung nötig.
Beispiel: Beendigungs-Flag
public class Worker extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
// etwas Nützliches erledigen
}
System.out.println("Thread beendet");
}
public void shutdown() {
running = false;
}
}
Worker w = new Worker();
w.start();
// ... nach einiger Zeit
w.shutdown();
Ohne volatile kann der Thread die Änderung des Flags „nicht bemerken“ und endlos weiterlaufen (insbesondere auf Mehrkernsystemen). Mit volatile funktioniert alles wie gewünscht.
5. Einschränkungen von volatile: Nicht-Atomarität
Viele Einsteiger denken: „Wenn man einen volatile int hat, kann man count++ schreiben und muss sich keine Gedanken machen.“ Leider ist das nicht so:
private volatile int count = 0;
public void increment() {
count++;
}
Fehler! Die Operation count++ ist dennoch nicht atomar – das sind drei Schritte: (1) lesen, (2) erhöhen, (3) zurückschreiben. Wenn zwei Threads gleichzeitig denselben Wert lesen, erhöhen beide ihn und schreiben beide dasselbe Ergebnis zurück – ein Inkrement „geht verloren“.
Fazit: volatile gewährleistet nur die Sichtbarkeit von Änderungen, schützt aber nicht vor Race Conditions bei komplexen Operationen.
6. Wann synchronized und wann – volatile einsetzen
- volatile – wenn Sie ein einfaches Flag haben (z. B. boolean), das ein Thread schreibt und ein anderer liest. Beispiel: Beenden eines Threads, Signalisierung eines Ereignisses.
- synchronized – wenn die Atomarität komplexer Operationen sichergestellt werden muss (z. B. Inkrement, Änderung mehrerer Variablen, Arbeit mit Datenstrukturen).
Merktabelle
| Szenario | volatile | synchronized |
|---|---|---|
| Signal zwischen Threads übertragen | ✔ | ✔ |
| Atomare Operation (Inkrement) | ✖ | ✔ |
| Mehrere Schritte im kritischen Abschnitt | ✖ | ✔ |
| Nur Sichtbarkeit der Änderungen | ✔ | ✔ |
7. Typische Fehler beim Einsatz von synchronized und volatile
Fehler Nr. 1: Synchronisation auf dem falschen Objekt. Wenn Sie auf einer lokalen Variablen oder auf unterschiedlichen Objekten in jedem Thread synchronisieren – gibt es keinen Schutz.
Object lock = new Object();
synchronized (lock) {
// ...
}
Wenn jeder Thread sein eigenes lock erstellt – bringt das nichts. Es braucht einen gemeinsamen Synchronisationspunkt, ein gemeinsames Objekt für alle Threads.
Fehler Nr. 2: Von volatile Atomarität erwarten. volatile garantiert Sichtbarkeit, nicht Atomarität. Operationen wie count++ sind ohne Synchronisierung weiterhin unsicher.
Fehler Nr. 3: Zu großer synchronisierter Codebereich. Wenn Sie die gesamte Methode synchronisieren, obwohl nur eine Zeile nötig wäre, blockieren Sie unnötig andere Threads und verlieren Leistung. Versuchen Sie, den „kritischen Abschnitt“ zu verkleinern.
Fehler Nr. 4: Synchronisation für statische Daten nicht „statisch“ gemacht. Wenn Sie eine statische Variable haben, aber auf this synchronisieren, hilft das nicht. Für statische Daten ist Synchronisation auf Klassenebene nötig: synchronized(ClassName.class).
Fehler Nr. 5: Synchronisation auf String-Literal. Synchronisation auf Strings ist gefährlich, weil identische Literale von der JVM interniert werden. Man kann versehentlich eine gemeinsame Sperre für unterschiedliche Programmteile erhalten.
GO TO FULL VERSION