«Was bedeutet synchronized?» – eine Frage, die jeden zweiten Junior quält
\n\nWenn Sie jemals eine nebenläufige Anwendung in Java geschrieben haben, sind Sie sicherlich auf folgende Situation gestoßen: Zwei Threads ändern gleichzeitig eine Variable, und das Ergebnis sind Bugs, die sich nur einmal in hundert Läufen reproduzieren lassen. Kommt Ihnen bekannt vor? Genau dafür gibt es das Schlüsselwort synchronized. Aber was macht es eigentlich? Wie funktioniert es «unter der Haube»? Und vor allem – wann sollte man es verwenden und wann besser darauf verzichten?
In diesem Artikel wiederholen wir nicht einfach die Dokumentation. Wir analysieren eine beliebte Frage von Stack Overflow (diejenige, die tausende Aufrufe gesammelt hat), passen sie an ein Bildungsformat an und zeigen einen Vergleich der Ansätze: Synchronisation auf Methodenebene, auf Blockebene und Alternativen aus java.util.concurrent. Los geht's!
Das Problem: Wettlaufsituation (Race Condition)
\n\nStellen Sie sich vor: Sie haben einen Zähler, der von zwei Threads inkrementiert wird. Der Code ist einfach, aber das Ergebnis ist unerwartet.
\n\npublic class Counter {\n private int count = 0;\n\n public void increment() {\n count++;\n }\n\n public int getCount() {\n return count;\n }\n}\n\nWir starten zwei Threads, jeder führt 1000 Inkremente durch. Man würde 2000 erwarten. In Wirklichkeit sind es aber 1987, 1993 oder sogar 1765. Warum? Weil die Operation count++ nicht atomar ist. Sie besteht aus drei Schritten: Lesen → Erhöhen → Schreiben. Die Threads «überschneiden» sich, und eine Änderung geht verloren.
Das ist eine race condition – eine Wettlaufsituation. Und synchronized ist eines der wichtigsten Werkzeuge, um dagegen vorzugehen.
Was macht synchronized? Ein Blick unter die Haube
\n\nDas Schlüsselwort synchronized in Java garantiert gegenseitigen Ausschluss (Mutex) und Sichtbarkeit von Änderungen zwischen Threads. Wenn ein Thread in einen synchronized-Block eintritt, erfasst er den Monitor des Objekts (oder der Klasse). Andere Threads, die versuchen, in einen synchronized-Block mit demselben Monitor einzutreten, werden blockiert, bis er freigegeben wird.
Wichtig: synchronized funktioniert nur mit einem Monitor. Wenn Sie zwei verschiedene Objekte haben, funktioniert die Synchronisation nicht.
Zwei Ebenen der Synchronisation
\n- \n
- Synchronisierte Methode – blockiert die gesamte Methode, verwendet den Monitor des Objekts (
this). \n - Synchronisierter Block – blockiert nur einen Teil des Codes, Sie können jedes Objekt als Monitor angeben. \n
Vergleichen wir sie in der Praxis.
\n\nVergleich der Ansätze: Methode vs. Block
\n\n1. Synchronisierte Methode
\n\nDer einfachste Weg ist, synchronized zur Methodensignatur hinzuzufügen.
public class SafeCounter {\n private int count = 0;\n\n public synchronized void increment() {\n count++;\n }\n\n public synchronized int getCount() {\n return count;\n }\n}\n\nVorteile:
\n- \n
- Minimaler Code, leicht lesbar. \n
- Garantiert, dass die gesamte Methode ein atomarer Vorgang ist. \n
Nachteile:
\n- \n
- «Schwere» Sperre – selbst wenn die Methode nur eine Zeile enthält, wird der Thread für die gesamte Ausführungszeit blockiert. \n
- Verringert die Leistung, wenn die Methode lange dauert oder häufig aufgerufen wird. \n
- Nicht flexibel – Sie können nicht nur einen Teil der Logik synchronisieren. \n
2. Synchronisierter Block
\n\nFeinere Kontrolle. Sie entscheiden selbst, welches Objekt als Monitor verwendet wird.
\n\npublic class SafeCounterWithBlock {\n private int count = 0;\n private final Object lock = new Object();\n\n public void increment() {\n synchronized (lock) {\n count++;\n }\n }\n\n public int getCount() {\n synchronized (lock) {\n return count;\n }\n }\n}