Java Synchronisierung
Thread-Zugriff auf gemeinsamen Zustand in Java mit dem Schlüsselwort synchronized und intrinsischen Sperren koordinieren.
Die Multithreading-Einführung warnte vor drei Fehlerarten — Wettlaufbedingungen, Sichtbarkeitsfehler und Deadlocks. synchronized ist Javas erste Antwort auf die ersten beiden. Es gibt einem Codeblock gleichzeitig zwei Garantien: gegenseitiger Ausschluss (nur ein Thread darf sich zu einem Zeitpunkt darin befinden) und Speichersichtbarkeit (Schreibvorgänge, die ein Thread innerhalb des Blocks ausführt, sind für den nächsten Thread sichtbar, der ihn betritt). Diese beiden Garantien zusammen reichen aus, um eine große Menge von Multithread-Code korrekt zu machen.
Dieses Kapitel ist das konzeptionelle — was synchronized macht, was ein intrinsischer Monitor ist, welche Arten von Wettlaufbedingungen es behebt und welche nicht. Das nächste Kapitel, synchronized blocks, zeigt die syntaktischen Formen und wie man zwischen ihnen wählt.
Die Wettlaufbedingung, für die das Schlüsselwort existiert
class Counter {
int n;
void increment() { n++; }
}
Counter c = new Counter();
// Thread A and Thread B both call c.increment() a million times.
// After both finish, what is c.n?n++ ist eine Quellzeile und drei Bytecode-Operationen: n laden, 1 addieren, n speichern. Wenn Thread A n=42 lädt, dann Thread B n=42 lädt, bevor A speichert, addieren beide 1 und speichern beide 43. Ein Inkrement geht verloren. Führt man das Programm aus, wobei jeder Thread eine Million Mal läuft, ist c.n konsistent kleiner als 2_000_000.
synchronized ist die Lösung:
class Counter {
int n;
synchronized void increment() { n++; }
}Jetzt führt nur ein Thread gleichzeitig increment auf diesem Counter aus. Der andere wartet vor der Tür. Ergebnis: c.n == 2_000_000, bei jedem Durchlauf.
Was ein Monitor ist
Jedes Java-Objekt hat im JVM verborgen eine zugehörige Sperre, den sogenannten intrinsischen Monitor (oder Monitor-Lock). Es ist lediglich eine long-artige Datenstruktur mit zwei Zustandsteilen: einem besitzenden Thread (oder null) und einer Warteschlange. Ein Thread, der einen synchronized-Block betritt:
- Versucht den Monitor des Objekts zu erwerben, auf das
synchronizedzeigt. - Wenn der Monitor unbesessen ist, nimmt er ihn (jetzt
owner == self) und macht weiter. - Wenn der Monitor einem anderen Thread gehört, wechselt dieser Thread in den Zustand
BLOCKEDund reiht sich in die Warteschlange ein. - Wenn der Besitzer den Block verlässt, gibt das JVM den Monitor frei und einer der Wartenden gewinnt ihn.
Der Monitor gehört zum Objekt. Zwei Counter-Instanzen haben zwei separate Monitore; Threads, die auf verschiedenen Counter-Instanzen arbeiten, blockieren sich nicht gegenseitig. Das ist wichtig — die Synchronisierung gilt für das Objekt, nicht für "die Methode."
synchronized (someObject) {
// critical section: only one thread at a time
// holds someObject's monitor inside this block
}synchronized bei einer Instanzmethode ist syntaktischer Zucker für synchronized (this). Bei einer statischen Methode ist es Zucker für synchronized (Counter.class) — den Monitor des Class-Objekts.
Sichtbarkeit, nicht nur Ausschluss
Gegenseitiger Ausschluss ist der offensichtliche Teil. Der weniger offensichtliche — und wichtigere — Teil ist die happens-before-Beziehung, die das JVM kostenlos bietet:
Alles, was ein Thread tut, bevor er einen Monitor freigibt, ist garantiert für jeden Thread sichtbar, der danach denselben Monitor erwirbt.
Dieser Satz macht synchronized korrekt und nicht nur "Wer zuerst kommt, mahlt zuerst." Ohne ihn können zwei Threads einen synchronized-Block verwenden, gegenseitigen Ausschluss einhalten und trotzdem gegenseitige Schreibvorgänge in der falschen Reihenfolge sehen — weil CPU-Caches und der JIT ansonsten frei umsortieren dürfen. Das Release/Acquire-Paar setzt eine Speicherbarriere, die CPU und JIT zwingt, zu leeren und neu zu laden.
Die Konsequenz: Jedes Feld, das ein Multithread-Programm außerhalb eines synchronized-Blocks liest oder schreibt (und nicht über volatile, ein Atomic oder ein anderes java.util.concurrent-Primitiv), hat keine Sichtbarkeitsgarantie. Ein Thread kann done = true schreiben und ein anderer Thread kann für immer done = false sehen. Wir kommen darauf zurück, wenn wir volatile und das Java-Speichermodell behandeln.
Was synchronized nicht behebt
Vier Dinge, die Neulinge oft von synchronized erwarten, die es aber nicht liefert:
- Es sperrt keine Daten.
synchronized (list)verhindert nicht, dass anderer Codelistberührt; es verhindert, dass ein anderer Thread denselben Monitor hält. Wenn ein anderer Codepfadlistbetreibt, ohne denselben Monitor zu erwerben, ist der Schutz aufgehoben. - Es komponiert nicht über Objekte.
synchronized (a); synchronized (b);sind zwei separate Erwerbungen; wenn ein anderer Thread sie in umgekehrter Reihenfolge erwirbt, entsteht ein Deadlock. - Es beschleunigt nichts. Sperren sind reiner Overhead. Verwende sie nur, wo Korrektheit es erfordert.
- Es behebt nicht alle Wettlaufbedingungen. Zusammengesetzte Aktionen wie "prüfen dann handeln" haben immer noch Wettlaufbedingungen, selbst wenn jede einzelne Operation synchronisiert ist.
if (map.containsKey(k)) map.put(k, v)ist fehlerhaft, selbst wenncontainsKeyundputeinzeln thread-sicher sind — die Lücke zwischen den beiden Aufrufen ist ungeschützt. VerwendeputIfAbsentoder einen einzigen synchronisierten Block um beide.
Wiedereintritt (Reentrancy)
Der intrinsische Monitor ist wiedereintrittsfähig: Ein Thread, der bereits einen Monitor hält, kann einen weiteren synchronized-Block auf demselben Objekt betreten, ohne sich selbst zu blockieren. Deshalb funktioniert das:
class Account {
synchronized void deposit(int x) { balance += x; }
synchronized void transferTo(Account other, int x) {
deposit(-x); // re-enters same monitor — fine
other.deposit(x); // acquires other's monitor too
}
}Wären Monitore nicht wiedereintrittsfähig, würde der innere Aufruf von deposit am Monitor blockieren, den der äußere Aufruf bereits hält — sofortiger Selbst-Deadlock. Die Wiedereintrittsfähigkeit macht es sicher, eine andere synchronisierte Methode auf demselben Objekt aufzurufen.
Die Kehrseite: Jeder Erwerb benötigt eine entsprechende Freigabe. Das JVM führt einen Zähler; der Monitor wird freigegeben, wenn der Zähler auf null fällt.
Worauf synchronisiert werden soll
Einige Regeln, die die meisten Fehler bei der Sperrenverwendung verhindern:
- Synchronisiere auf ein privates Sperrobjekt, nicht auf
this. Externer Code kann ebenfallssynchronized (yourInstance)verwenden; das erlaubt einem Aufrufer, deine Sperre so lange zu halten, wie er möchte. Ein privatesfinal Object lock = new Object();gehört dir allein und niemand sonst kann es sich greifen. - Synchronisiere nicht auf
String-Literale oder geboxt primitive Typen. Sie werden intern zwischengespeichert; zweisynchronized ("foo")-Blöcke an verschiedenen Stellen des Codes teilen einen Monitor mit jedem anderen, der ebenfalls"foo"gesagt hat. - Synchronisiere nicht auf eine Referenz, die sich ändern kann.
synchronized (myField), womyFieldneu zugewiesen werden kann, sind im Laufe der Zeit zwei verschiedene Monitore. Der Compiler kann das nicht erkennen; der Fehler ist still. - Halte den kritischen Abschnitt klein. Je mehr du innerhalb eines
synchronized-Blocks tust, desto länger warten alle anderen. Halte die Sperre, während du den gemeinsamen Zustand änderst, nicht während du die umgebenden I/O-Operationen ausführst.
Ein ausgearbeitetes Beispiel: mit und ohne Sperre
Das folgende Programm führt dieselbe gemeinsame Zähler-Arbeitslast auf drei Arten aus: ohne Synchronisierung, mit synchronized-Methode und mit synchronized-Block auf einem dedizierten Sperrobjekt. Die Zahlen zeigen, dass die erste Form Aktualisierungen verliert, die anderen beiden jedoch nicht.
Was aus dem Durchlauf zu entnehmen ist:
- Die
unsafe-Zeile verlor konsistent Aktualisierungen — der Endwert lag irgendwo unter dem erwarteten1_000_000. Zwei Threads, dien++ausführen, haben eine Wettlaufbedingung bei Lesen-Modifizieren-Schreiben; manche Inkremente verschwinden. Selbst wenn der Test bei einem einzigen Durchlauf zufällig besteht, werden der JIT, der OS-Scheduler oder eine andere CPU es irgendwann aufdecken. Unsynchronisierte Mutation eines gemeinsamen Felds ist fehlerhaft. - Beide sicheren Varianten erzeugten jedes Mal die exakt erwartete Zählung. Gegenseitiger Ausschluss ist der offensichtliche Teil dessen, was
synchronizedtut; der weniger sichtbare Teil ist, dass der Wert, denvalue()liest, der neueste vonincrementgeschriebene ist — das ist die Sichtbarkeitsgarantie. Ohne das Monitor-Paar könnte der Lesevorgang legitimerweise eine veraltete gecachte Kopie sehen. - Die Wanduhrzeit für
sync methodundsync blockwar beide deutlich höher alsunsafe. Sperren sind nicht kostenlos — jeder Ein-/Austritt setzt eine Speicherbarriere und (unter Konkurrenz) einen Thread-Kontextwechsel. Synchronisiere, wo Korrektheit es erfordert; streue nicht aus "Sicherheitsgründen." - Die Variante
sync block on private lockist das, was Produktionscode verwendet. Diesync method-Form sperrt aufthis, das jeder externe Aufrufer ebenfalls erwerben kann — er kann dich aushungern, indem er deine eigene Sperre hält. Ein privates Sperrobjekt, das du niemals freigibst, gehört dir allein. - Der Wiedereintrittsblock lief ohne Deadlock.
outer()hielt bereits den Monitor vonthis;inner()betrat ihn erneut, ohne zu blockieren. Deshalb kann eine synchronisierte Methode frei eine andere synchronisierte Methode auf demselben Objekt aufrufen — ohne Wiedereintritt würde die halbe Standardbibliothek in einem Deadlock enden.
Was als nächstes kommt
Das nächste Kapitel, Java Synchronized Blocks, vertieft die syntaktischen Formen — Methode, Block, statisch — und die Regeln zur Wahl des richtigen Sperrobjekts. Für die übergeordnete Koordination zwischen Threads, siehe Inter-Thread-Kommunikation (wait/notify).