Java Memory Model
Das Java Memory Model – wann Schreibzugriffe zwischen Threads sichtbar sind und wie happens-before funktioniert.
Das Java Memory Model (JMM) ist der Teil der Sprachspezifikation, der definiert, wann ein Thread garantiert die Schreibzugriffe eines anderen Threads sieht. Es ist das Regelwerk hinter volatile, synchronized und final – und der Grund, warum korrekter Multithread-Code so aussieht, wie er aussieht.
Dieses Kapitel erklärt, warum das Modell existiert, wie die happens-before-Beziehung alles zusammenhält und welches Werkzeug bei Sichtbarkeits-, Atomizitäts- oder Umordnungsproblemen eingesetzt werden sollte.
Warum das Memory Model existiert
Auf moderner Hardware kann der Wert, den ein Thread in ein Feld „schreibt", lange in einem CPU-Register oder Core-lokalem Cache verbleiben, bevor er den Hauptspeicher erreicht. Zudem darf der Compiler unabhängige Anweisungen umordnen. Ohne Regeln könnte ein Thread ein Feld setzen, während ein anderer Thread die Änderung nie sieht – oder sie in falscher Reihenfolge sieht.
Das JMM definiert eine einzige Garantie, die all das bändigt: die happens-before-Beziehung. Wenn Aktion A happens-before Aktion B gilt, sind die Effekte von A für B sichtbar. Alles andere – volatile, Locks, final, Thread-Starts und Joins – ist nur eine Möglichkeit, eine happens-before-Kante zu erzeugen.
// Without synchronization, this loop may NEVER terminate:
// the reader thread can cache 'running' forever and miss the write.
static boolean running = true; // plain field — no guarantee
void reader() { while (running) { /* spin */ } } // may hang
void stopper() { running = false; } // may go unseenDas volatile-Schlüsselwort
Ein Feld als volatile zu deklarieren bewirkt zwei Dinge: Jeder Lesevorgang geht direkt in den Hauptspeicher (Sichtbarkeit), und ein volatile-Schreibzugriff gilt happens-before jedem späteren volatile-Lesevorgang desselben Feldes (Ordnung). Es macht zusammengesetzte Aktionen wie count++ jedoch nicht atomar.
public class Worker {
private volatile boolean running = true; // visible across threads
public void run() {
while (running) { // always sees the latest value
doWork();
}
}
public void stop() {
running = false; // guaranteed visible to run()
}
}Verwende volatile für ein einzelnes Flag oder eine Referenz, die von vielen Threads gelesen und von einem Thread geschrieben wird. Greife darauf zurück, wenn Sichtbarkeit benötigt wird, nicht gegenseitiger Ausschluss. Weitere Einzelheiten findest du unter Java volatile.
Happens-Before: Die Kernregeln
Happens-before ist der Vertrag, gegen den du tatsächlich programmierst. Diese Kanten werden bewusst erzeugt:
| Regel | Happens-before-Kante |
|---|---|
| Programmreihenfolge | Jede Aktion in einem Thread gilt happens-before späterer Aktionen im selben Thread |
| Monitor-Lock | Das Freigeben eines Monitors gilt happens-before einem späteren Sperren desselben Monitors |
| Volatile | Ein Schreibzugriff auf ein volatile-Feld gilt happens-before jedem späteren Lesevorgang davon |
| Thread-Start | thread.start() gilt happens-before jeder Aktion im gestarteten Thread |
| Thread-Join | Alle Aktionen eines Threads gelten happens-before einem anderen Thread, der von seinem join() zurückkehrt |
| final-Felder | Schreibzugriffe des Konstruktors auf final-Felder gelten happens-before der Veröffentlichung des Objekts |
// synchronized creates a happens-before edge through the same lock:
synchronized (lock) { shared = compute(); } // unlock here ...
// ... happens-before another thread's:
synchronized (lock) { use(shared); } // ... lock hereAtomizität vs. Sichtbarkeit
Dies sind zwei verschiedene Probleme, die unterschiedliche Werkzeuge erfordern. volatile behebt Sichtbarkeit, aber nicht Atomizität; synchronized und die Klassen aus java.util.concurrent.atomic beheben beides für den jeweiligen Bereich.
| Problem | Symptom | Lösung |
|---|---|---|
| Sichtbarkeit | Ein Thread sieht einen aktualisierten Wert nie | volatile, synchronized, final |
| Atomizität | Verlorene Updates durch x++ unter Konkurrenz | synchronized, AtomicInteger, Locks |
| Umordnung | Operationen erscheinen in falscher Reihenfolge | happens-before durch die oben genannten Werkzeuge |
import java.util.concurrent.atomic.AtomicLong;
public class Counter {
private final AtomicLong hits = new AtomicLong();
public void record() { hits.incrementAndGet(); } // atomic + visible
public long total() { return hits.get(); }
}final-Felder und sichere Veröffentlichung
Ein final-Feld, das im Konstruktor gesetzt wird, ist eingefroren, sobald der Konstruktor zurückkehrt. Jeder Thread, der ein korrekt konstruiertes Objekt sieht (eines, dessen Referenz nicht aus dem Konstruktor entwichen ist), hat garantiert korrekte Werte für seine final-Felder – ohne volatile oder Lock. Deshalb sind unveränderliche Objekte von Natur aus thread-sicher.
public final class Point {
private final int x, y; // frozen at construction
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
}
// Share a Point across threads freely: its final fields are safely published.Ein eigenständiges Beispiel
Das ausführbare Beispiel unten verwendet ausschließlich das JDK. Es übt vier Memory-Model-Werkzeuge in einem Programm: volatile für thread-übergreifende Sichtbarkeit, AtomicInteger für verlustfreies Zählen, final-Felder für sichere Veröffentlichung und synchronized für atomare Akkumulation.
Was aus der Ausführung hervorgehen sollte:
reader saw data = 42beweist, dass dervolatile-Schreibzugriff aufflagden einfachen Schreibzugriff aufdataveröffentlicht hat – der Reader sieht ihn garantiert aufgrund der happens-before-Kante.atomic counter = 800000 (expected 800000)zeigt, dassAtomicInteger.incrementAndGet()über 8 Threads mit je 100.000 Inkrementen keine Updates verloren hat – ein einfachesint++würde eine kleinere, nicht-deterministische Zahl ausgeben.final config = prod:443demonstriert sichere Veröffentlichung: Diefinal-Felder vonConfigsind korrekt ohnevolatileoder Lock.synchronized sum = 10000bestätigt, dass die vier Writer (1000+2000+3000+4000) ohne verlorene Additionen durch denselben Monitor akkumuliert haben.- Jede Ausgabezeile entspricht einem anderen happens-before-Mechanismus, dennoch lassen sie sich in einem Programm kombinieren – die JMM-Werkzeuge ergänzen sich, sind aber nicht austauschbar.
Das richtige Werkzeug wählen
Eine kurze Entscheidungshilfe, wenn ein Memory-Model-Mechanismus benötigt wird:
- Ein Writer, viele Reader eines Flags oder einer Referenz? Verwende
volatile. - Zähler, Akkumulatoren oder Compare-and-Set auf einer einzelnen Variable? Verwende die Atomic-Klassen – sie vermeiden Lock-Overhead.
- Ein mehrstufiges Update, das alles-oder-nichts sein muss? Schütze es mit
synchronized(oder einem expliziten Lock). - Schreibgeschützten Zustand teilen? Mache ihn unveränderlich mit
final-Feldern; siehe Immutable classes. Keinvolatileoder Lock erforderlich.
Verwandte Kapitel
- Java
volatile– das Sichtbarkeits-Schlüsselwort im Detail. - Java Synchronization – gegenseitiger Ausschluss und der Monitor-Lock.
- Atomic Variables – lock-freie atomare Operationen.
final-Schlüsselwort und Immutable Classes – sichere Veröffentlichung durch Konstruktion.