Vadym Kazulkin ip.labs GmbH

Der Java-Code selbst ist über Jahrzehnten großteils unverändert geblieben. Die Komplexität der dahinterliegenden Hardware wurde aber währenddessen kontinuierlich erhöht.

Rodion Alukhanov ip.labs GmbH

„Write once – run everywhere“ klingt zwar einfach, ist aber in der Praxis nicht trivial umzusetzen, denn viele Hardware-Memory-Modelle haben ihre eigenen Besonderheiten.

Die Spezifikation des Java Memory Model gehört zu der kompliziertesten im Java-Umfeld, deren Verständnis ist aber in Zeiten von Mehrkernprozessoren extrem wichtig, um Java-Applikationen zu schreiben, die Nebenläufigkeit korrekt unterstützen.

Mit Java 9 dürfte ein Update des Java Memory Models zu erwarten sein. Zu diesem Anlass haben wir uns entschieden, das Thema zu beleuchten. Im ersten Teil der Artikelserie geben wir einen kurzen Überblick über die verschiedenen Hardware-Memory-Modelle, was auf den ersten Blick ungewöhnlich erscheint. Wieso ist uns das wichtig? Das Versprechen, das uns Java gibt „Write once – run everywhere“, klingt zwar einfach, ist aber in der Praxis nicht trivial umsetzbar, denn viele Hardware-Memory-Modelle haben ihre eigenen Besonderheiten. Deren Komplexität will die Spezifikation des Java Memory Model vor uns nur verstecken, indem sie eine Abstraktionsschicht definiert. Viele würden das Java Memory Model als sehr komplex erachten – ist es auch. Dafür gibt es Gründe, die wir in dieser Artikelserie näher beleuchten wollen. Zuerst schauen wir uns ein einfaches Beispiel aus Listing 1 an.

 
public class MyClass {
  int x, y;
  public void executeOnCPU1() {
    x = 1;
    x = 2;
    y = 1;
    x = 3;
  }
  public void executeOnCPU2() {
    System.out.println("x: "+x+ " y: "+y);
  }
}
Ende

Wir sehen in der Klasse zwei Methoden und gehen davon aus, dass executeOnCPU1 und executeOnCPU2 parallel von unterschiedlichen Threads auf unterschiedlichen Prozessoren ausgeführt werden. Da keinerlei Synchronisation im Code stattfindet, stellen wir nun die Frage: Wenn in der Ausgabe der Methode executeOnCPU2 y=1 erscheint, welche Werte von x könnten in der gleichen Zeile ausgegeben werden? Bei Werten wie x=2 und x=3 sollte jedem klar sein, was das zu erwartende Ergebnis ist. Aber sind auch die Werte x=0 oder x=1 möglich? Wenn wir aber die Fragestellung präzisieren und ergänzen, dass das Programm auf einem x86-Prozessor läuft, ändert sich dadurch bei der Antwort irgendetwas? Und wenn wir es auf dem Android-Gerät mit dem ARMv7 laufen lassen? Gibt es Tools, mit denen wir das verlässlich testen können? Wir haben schon sehr viele Fragen, die wir alle im Rahmen dieser Artikelserie beantworten werden. Wir fangen aber mit etwas Theorie an.

L1, L2, L3: die Memory-Hierarchie

Abbildung 1 ist eine einfache Darstellung der Memory-Hierarchie. Wir sehen, dass jeder Prozessor eigene L1-(First-Level-) und L2-(Second-Level-)Caches besitzt. Es gibt noch einen optionalen prozessorübergreifenden L3-Cache. Der L1-Cache besteht häufig aus Instruction und Data-Caches.

Abb. 1: Memory-Hierarchie

Abb. 1: Memory-Hierarchie

In den Caches werden die Informationen gespeichert, die der Prozessor häufig und in naher Zukunft für seine Berechnungen benötigt. Bei der Suche nach Information geht der Prozessor die Caches in der Reihenfolge L1, L2, L3 nacheinander durch. Falls in keinem der Caches die vom Prozessor gesuchte Information zu finden ist (Cache miss), findet ein Zugriff auf den Hauptspeicher statt. Generell gilt: Je näher der Cache am Prozessor ist, desto kleiner ist er. Wie groß die Caches tatsächlich sind, kann man unter Linux mithilfe von lscpu ermitteln, für Windows empfiehlt sich die Nutzung des Tools CPU-Z. Normalerweise ist der L1-Cache mindestens 64 KB (32 KB je für Instruction und Data-Cache), der L2-Cache mindestens 256 KB und der optionale L3-Cache mindestens 2 MB groß. Einen sehr guten Überblick über die Arbeitsweise der Caches bietet der Artikel unter. An dieser Stelle sind noch die Zugriffszahlen wichtig: Je näher man am Prozessor ist, desto schneller ist der Zugriff. Diese Zugriffszeiten können z. B. unter ermittelt werden. Den für uns relevanten Ausschnitt zeigt Abbildung 2. Wir sehen, dass der Zugriff auf den L1-Cache 1 ns dauert, auf den L2-Cache 4 ns und auf den Hauptspeicher sogar 100 ns. Daraus erkennen wir, dass die Zugriffe auf den Hauptspeicher vergleichsweise teuer sind.

Abb. 2: Zugriffszeiten auf L-Caches und RAM

Abb. 2: Zugriffszeiten auf L-Caches und RAM

Cachekohärenz

Bei den Cachehierarchien auf einer Prozessorebene stellt sich folgende Frage: Wie kann eine Änderung im L1-Cache des ersten Prozessors dazu führen, dass Prozessor 2 über den veränderten Wert informiert wird? Oder generell gefragt: Wie wird die Konsistenz der Caches sichergestellt, sodass die Prozessoren darin die aktuellen Werte vorfinden? An dieser Stelle führen wir den Begriff der Cachekohärenz ein: Cachekohärenz ist die Konsistenz der verteilten Daten, die in vielen lokalen Caches gespeichert sind.

Artikelserie
Teil 1: Hardware-Memory-Modelle reloaded
Teil 2: Das Java Memory Model
Teil 3: Das Open JDK Java Concurrency Stress Tests Tool
Teil 4: Die Zukunft des Java Memory Models

Den vollständigen Artikel lesen Sie in der Ausgabe:

Java Magazin 4.17 - "APIs"

Alle Infos zum Heft
579777421Das Java Memory Model von der Hardwareseite betrachtet
X
- Gib Deinen Standort ein -
- or -