AngelikaLanger.com - An Overview of the Java Memory Model (JMM) (original) (raw)

An Overview of the Java Memory Model (JMM)

Java Memory Model: Überblick Das Java-Memory-Modell im Überblick Java Magazin, August 2008 Klaus Kreft & Angelika Langer Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist. Die übrigen Artikel dieser Serie sind ebenfalls verfügbar (click here). Die ganze Serie zum "Java Memory Modell" als PDF(985 KB).

Im letzten Beitrag unserer Reihe über Aspekte des Java-Memory-Modells (JMM) haben wir erläutert, dass man das volatile-Schüsselwort zum Zwecke der Optimierung verwendet, um die relative teure Synchronisation von konkurrierenden Zugriffen auf gemeinsam verwendete veränderliche Daten zu vermeiden. Dabei haben wir die Sichtbarkeitsregeln erwähnt, die sich im Zusammenhang mit volatile und Synchronisation aus dem Java-Memory-Modell ergeben (siehe /JMM1/). In diesem Beitrag wollen wir einen Überblick über das Memory-Modell geben.

Im Zusammenhang mit volatile und Synchronisation haben wir den Begriff der Sequential Consistency erwähnt. Das ist ein Modell, mit dem sich viele Java-Entwickler das Multithreading in Java vorstellen, obwohl Java gar keine Sequential Consistency unterstützt. Wenn man dennoch so programmiert, als gäbe es Sequential Consistency in Java, dann können Fehler entstehen wie jener, den wir im letzten Beitrag besprochen haben.

Das Modell der Sequential Consistency ist ein relativ einfaches Datenkonsistenzmodell. Es ähnelt der Funktionsweise von Multithreading auf einer Single-CPU-Umgebung. Die Vorstellung ist: die Threads laufen nicht wirklich parallel, sondern es gibt einen Thread-Scheduler, der den einzelnen Threads abwechselnd Zeitscheiben der CPU zuteilt. Ein Thread darf ein paar Operationen ausführen, wird dann verdrängt, es kommt ein anderer Threads dran, der wiederum ein paar Operationen machen darf, usw., so dass die einzelnen Threads ihre Operationen in einer sequenziellen Reihenfolge ausführen. Damit verbunden ist die Vorstellung, dass Threads, die später drankommen, Modifikationen im Speicher sehen können, die von Threads vorgenommen wurden, die vorher dran waren. Das ist ein einfaches Konsistenzmodell, das aber in Java nicht unterstützt wird.

Bei einem Konsistenzmodell geht es ganz allgemein (unabhängig von Java) um Regeln für die Zugriffe auf den Speicher. Wenn sich der Programmierer an die Regeln hält, dann gibt ihm das System (in unserem Falle Java und seine virtuelle Maschine) Garantien für die Effekte von Speicherzugriffen, damit der Programmierer weiss, was zur Laufzeit geschehen wird, und er die Effekte seiner Speicheroperationen vorhersehen kann. In High-Level-Sprachen wie Java müssen der Compiler und das Laufzeitsystem dafür sorgen, dass die High-Level-Sprachkonstrukte gemäß den Regeln des Konsistenzmodells in Low-Level-Operationen umgesetzt werden.

Auch Java hat ein Konsistenzmodell. Es ist aber nicht das Modell der Sequential Consistency, sondern Java hat ein eigenes Memory-Modell, das als JMM (= Java Memory Model) bezeichnet wird. Seine Regeln sind deutlich anders und schwächer als die der Sequential Consistency.

Wir wollen in diesem Beitrag einen Überblick über die Regeln des JMM geben. Das Thema ist ein wenig theoretisch und es ist nicht unmittelbar einsichtig, was all die Regeln für die Praxis der Java-Programmierung bedeuten. Trotzdem wollen wir erst einmal einen Überblick über das Modell als solches geben, ehe wir in nachfolgenden Beiträgen dieser Reihe die einzelnen Aspeke noch einmal näher auf ihre Bedeutung für die Praxis untersuchen.

Das Java-Memory-Modell

Das Memory-Modell in Java ähnelt einer abstrakten SMP (= symmetric multi processing)-Maschine: die Threads laufen parallel und konzeptionell haben alle Threads Zugriff auf einen gemeinsamen Hauptspeicher (main memory), in dem die gemeinsam verwendeten Variablen abgelegt sind. Daneben hat jeder Thread einen eigenen lokalen Speicherbereich (cache), in den er Variablen hineinladen und lokal bearbeiten kann. Das Zurückschreiben der lokalen Daten in den Hauptspeicher (flush) und das Hereinladen von Daten aus dem Hauptspeicher (refresh) muss nach den Regeln des JMM geschehen. Das JMM beschreibt nun, in welcher Reihenfolge Aktionen passieren und welche Aktionen einen Flush oder Refresh auslösen.

Eine dieser Regeln besagt zum Beispiel, dass beim Start eines Threads alle relevanten Daten aus dem Hauptspeicher in den lokalen Arbeitsspeicher des Threads geladen werden. Dann darf der Thread mit diesen lokalen Daten arbeiten und muss gar nicht mehr ins Main Memory schauen, weil er die Daten im Cache hat. Am Ende des Threads muss der gesamte lokale Arbeitsspeicher des Threads wieder in den Hauptspeicher zurückgeschrieben werden. Daraus ergibt sich das Verhalten, dass wir auch intuitiv erwarten. Wenn ein Thread mit join auf das Ende eines anderen Threads wartet, kann der wartende Thread sehen, welche Modifikationen der andere, bereits beendete Thread gemacht hat.

Das JMM ist auch wieder nur ein Modell, mit dem sich der Java-Programmierer das Verhalten von Threads in einer JVM erklären kann. In Wirklichkeit muss die virtuelle Maschine die Regeln des JMM auf die Hardware abbilden, die ihr eigenes Hardware-Memory-Modell hat. Die heutigen Multicore-Prozessoren arbeiten nicht nur mit einem Cache pro Prozessorkern, sondern teilweise mit mehreren Ebenen von Caches und komplexeren Caching-Mechanismen, als sie das JMM vorsieht. Deshalb fällt der virtuellen Maschine die Aufgabe zu, mit geeigneten Anweisungen an die Hardware die Regeln des JMM zu implementieren.

Sichtbarkeitsregeln im JMM

Das Memory-Modell von Java regelt drei Dinge:

Bei der Atomicity geht es zum Beispiel darum, dass der Zugriff auf Variablen von primitivem Typ (außer long und double) sowie auf Referenzvariablen ununterbrechbar ist. Gleiches gilt für volatile-Variablen (diesmal inklusive long und double). Die Operationen auf atomaren Variablen im Package java.util.concurrent.atomic sind ununterbrechbar. Gleiches gilt für einige der Operationen der Concurrent Collections im Package java.util.concurrent, z.B. die Methode putIfAbsent der ConcurrentMap. Bei Referenzvariablen darf man nicht vergessen, dass stets nur der Zugriff auf die Referenz selbst, d.h. auf die Adresse, atomar ist, nicht etwa der Zugriff auf das referenzierte Objekt. Ansonsten sind die Regeln zur Atomarität relativ einfach zu verstehen.

Beim Ordering geht es darum, unter welchen Umständen ein Thread die Effekte von Operationen, die ein anderer Thread ausführt, in der Reihenfolge sehen kann, in der der andere Thread die Operationen vorgenommen hat. Im allgemeinen ist nämlich ein weitrechendes Re-Ordering erlaubt, an dem sich der Compiler, die virtuelle Maschine und die Hardware beteiligen. Das Re-Ordering führt dazu, dass zwar innerhalb eines Threads klar ist, in welcher Reihenfolge die Effekte der Operationen entstehen, aber ein anderer Thread, der sich das anschaut, sieht die Effekte u.U. in einer anderen Reihenfolge, als sie produziert wurden. Es gibt im JMM Regeln für das Ordering, aber sie sind komplex und damit schwer zu verstehen. Deshalb sehen wir uns das Ordering nicht jetzt, sondern in einem späterem Beitrag genauer an.

Bei der Visibility geht es darum, ob und wann Modifikation am Speicher, die ein Thread gemacht hat, den anderen Threads sichtbar werden. Die Sichtbarkeitsregeln des JMM geben eine Reihe von Garantien, von denen wir einige auch schon im letzten Artikel erwähnt haben:

Zusammenfassung

Wir haben uns angesehen, was das Java-Memory-Modell überhaupt ist; es geht dabei um Regeln für Speicherzugriffe. Wir haben daran erinnert, dass wir keine Sequential Consistency in Java haben, sondern stattdessen das Java-Memory-Modell (JMM) mit seiner SMP-artigen Vorstellung von Hauptspeicher und threadlokalen Arbeitsspeichern. Das JMM regelt Ununterbrechbarkeit von Operationen, Sichtbarkeit von Speichereffekten und die Reihenfolge von Operationen. Wir werden uns bei nächsten Mal überlegen, warum es überhaupt wichtig ist, das JMM und seine Regeln zu kennen - ehe wir uns in nachfolgenden Beiträgen in die Details vertiefen.

Literaturverweise und weitere Informationsquellen

Die gesamte Serie über das Java Memory Model:

/JMM1/ Einführung in das Java Memory Model: Wozu braucht man volatile? Klaus Kreft & Angelika Langer, Java Magazin, Juli 2008 URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/37.JMM-Introduction/37.JMM-Introduction.html
/JMM2/ Überblick über das Java Memory Model Klaus Kreft & Angelika Langer, Java Magazin, August 2008 URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/38.JMM-Overview/38.JMM-Overview.html
/JMM3/ Die Kosten der Synchronisation Klaus Kreft & Angelika Langer, Java Magazin, September 2008 URL: http://www.AngelikaLanger.com/Articles/39.JMM-CostOfSynchronization/39.JMM-CostOfSynchronization.html
/JMM4/ Details zu volatile-Variablen Klaus Kreft & Angelika Langer, Java Magazin, Oktober 2008 URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/40.JMM-volatileDetails/40.JMM-volatileDetails.html
/JMM5/ volatile und das Double-Check-Idiom Klaus Kreft & Angelika Langer, Java Magazin, November 2008 URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/41.JMM-DoubleCheck/41.JMM-DoubleCheck.html
/JMM6/ Regeln für die Verwendung von volatile Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2008 URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/42.JMM-volatileIdioms/42.JMM-volatileIdioms.html
If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar Concurrent Java - An in-depth seminar covering all that is worth knowing about concurrent programming in Java, from basics such as synchronization over the Java 5.0 concurrency utilities to the intricacies of the Java Memory Model (JMM). 4 day seminar (open enrollment and on-site)