Verstaendnisfragen zu Concurrency

sirbender

Top Contributor
Hallo,

ich habe einige Quellen zu Concurrency gelesen und immer stellen sich mir einige Fragen wo ich mir nicht sicher bin ob die Antwort die ich habe korrekt ist. Vielleicht koennt ihr mir kurz helfen:

1. ich fuege einer leeren Liste ein Element hinzu. Danach aendere ich ein "volatile boolean EMPTY" von true auf false. Alle Threads sehen nun, dass EMPTY false ist. Sehen damit auch alle Threads das Element in der Liste und ist die Liste konsistent zu diesem Zeitpunkt? Wenn ja, wie funktioniert das genau - das Schreiben der volatilen variablen zwingt alle CPUs ihre Caches zu leeren und ALLES (also auch die Liste) nochmal neu aus dem Hauptspeicher zu holen?
Funktioniert das gleiche bei einem synchronized Block? Wenn ich diesen mit einem Thread betrete passiert was mit den CPU Caches? Und was ist wenn ich den Block verlasse - werden dann die Aenderungen die im Block stattgefunden haben in den Hauptspeicher geschrieben und alle anderen Threads invalidieren ihre Caches und holen sich diese Werte erneut aus dem Hauptspeicher?

2. ein komplexes volatiles Objekt hat eine Vielzahl vom Feldern die wiederum komplexe Objekte sein koennen. Wenn ich dieses komplexe Objekt veraendere (also z.B. einem Feld einen neuen Wert zuweise bzw. wenn das Feld eine Liste ist ein Objekt der Liste hinzufuege), sehen dann alle Threads diese Aenderung?
Oder verspricht die das volatile lediglich, dass alle Threads sofort sehen wenn die volatile variable auf ein neues komplexes Objekt zeigt (also eine neue Referenz) aber nicht wenn ich Interna des komplexen Objekts veraendere - dies sehen alle Threads nicht sofort bzw. haben unterschiedliche Daten bzgl. der Interna?

3. wenn ich ein komplexes Objekt in einem synchronized Block veraendere sind alle Aenderungen danach fuer alle Threads sichtbar. Wie laeuft die Synchronisierung ab - irgendwie kann ich mir schwer vorstellen woher die CPU weiss, was alles zu diesem komplexen Objekt gehoert. Selbst die JVM sollte ihre Probleme haben, ansonsten waere sowas wie Serialisierung einfacher? Oder werden einfach alle Caches komplett invalidiert und alle Daten erneut aus dem Hauptspeicher geholt nachdem ein Thread in einem synchronized Block etwas gemacht hat (selbst wenn der Block keine Daten veraendert hat)?
 
Zuletzt bearbeitet:

httpdigest

Top Contributor
Ein paar hervorragende Ressourcen zum Lesen:
- http://www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf
- https://www.msully.net/blog/2015/02/25/the-x86-memory-model/
- https://preshing.com/20120930/weak-vs-strong-memory-models/
- https://preshing.com/20120913/acquire-and-release-semantics/
- https://www.cl.cam.ac.uk/~pes20/weakmemory/x86tso-paper.pdf
- https://stackoverflow.com/questions/1850270/memory-effects-of-synchronization-in-java#answer-1863612

Sowohl die virtuelle Maschine als auch eine reale Maschine, auf der die JVM implementiert ist, definieren ein sogenanntes "Memory Model". Es beschreibt die Sichtbarkeit und Reihenfolge von Speicheroperationen eines Programms bzw. Programmteilstücks in einer Multi-Threaded Ausführung. Das Java Memory Model beschreibt dies auf Basis von Java Syntaxelementen. Das Memory Model einer Prozessorarchitektur wie etwa x86 beschreibt dies auf den x86 Instruktionen.

Die JVM Implementierung muss nun sicherstellen, dass der von ihr generierte CPU-Code unter dem Memory Model der CPU mindestens so konsistent ist wie unter dem Memory Model der JVM. Und Tatsache ist: Das Memory Model von x86 ist sehr stark, fast schon sequenziell konsistent - das heißt, Schreiboperationen sind sofort für alle anderen Cores sichtbar und fast immer in der Reihenfolge der Schreibzugriffe. Hier wird auch von Cache Coherence gesprochen.

Das heißt, wenn eine x86 Instruktion in einem CPU-Core einen Wert an eine Speicheradresse schreibt, dann bekommen das alle anderen CPU-Cores sofort mit und alle Cores halten ihre Caches kohärent zueinander. Die Anwendung muss nirgends manuell irgendwelche Caches flushen oder vom Hauptspeicher nachladen. Das passiert automatisch durch das Memory Subsystem der CPU.
Was die Anwendung aber sehr wohl steuern kann, ist die Reihenfolge der Sichtbarkeit von Speicheroperationen (Stichwort: "Acquire" und "Release" Semantik von Instruktionen).
Und was Compiler (inklusive der JIT-Compiler der JVM) dürfen ist: Umsortieren der CPU Instruktionen, derart, dass die Zusicherungen des Memory Models noch erfüllt sind aber das Programm eventuell effizienter ausgeführt wird. Ebenso darf die CPU x86 Instruktionen bzw. Micro-Opcodes resortieren, um die Pipelines der CPU effizient zu füttern.
Tatsächlich ist das Memory Model unter x86 hauptsächlich dazu da, einem Programm (in diesem Fall dem JIT Compiler der JVM) zu diktieren, welche Umsortierungen auf den generierten x86 Instruktionen erlaubt sind und welche nicht - und weniger um zu wissen, wann welche Caches wie geflushed werden müssen -> müssen sie gar nicht).

Wie, wo und wann hier welche Caches geflushed oder nicht geflushed werden (oder ob es überhaupt Caches gibt), ist Aufgabe der Prozessorimplementierer und ist schon eine Abstraktionsebene unterhalb von x86. Die Prozessorfamilie x86 selbst definiert keinerlei Caches, sondern lediglich, welche Sichtbarkeitsregeln zwischen einzelnen x86 Instruktionen gelten. Caches und Puffer werden natürlich der Effizienz wegen eingeführt und wie die CPU das nun effizient sicherstellt, ist ihre Sache/Aufgabe.

Was kann die JVM also tun, um die durch das Java Memory Model vorgegebenen Sichtbarkeitsregeln von volatile und synchronized auf das Memory Model von x86 zu übertragen?

Sie muss tatsächlich nicht viel tun. Das Verlassen eines synchronized-Blocks muss lediglich sicherstellen, dass alle bis dorthin geschriebenen Variablen auch mittels x86 Speicherschreibzugriffen an das Memory Subsystem (letztlich an die Cache-Level und den Hauptspeicher) übergeben wurden und, dass alle eventuellen Write-Buffer geleert wurden (Acquire-Operation), also das Memory Subsystem tatsächlich bestätigt hat, dass die Speicheränderungen alle im Hauptspeicher bzw. den Caches der anderen CPU-Cores angekommen sind.

Die JVM muss also alle bis jetzt eventuell in Registern oder anderen Zwischenspeichern (x86 Hardwarestack) gehaltenen Variablen per Speicherschreiboperation in den Hauptspeicher zurückschreiben.
Da die JVM völlig frei ist, wo und wie sie eine gelesene Variable tatsächlich innerhalb eines Methodenframes zwischenspeichert (als Register, im Stackspeicher oder in einem anderen Hauptspeicherbereich), muss sie nun sicherstellen, dass die bis hierhin gehaltenen Werte auch in den eigentlichen Hauptspeicher zurückgeschrieben werden. Sie muss sich hierfür aber nicht "merken", welche Objekte bis hierhin verändert wurden. Der "Bereich", in dem sie sich so etwas nur merken muss, ist ein "Inline Scope", also der Scope einer Methodenausführung samt eventueller weiterer aufgerufener Methoden, für die die JVM weiß, welche Variablen hier gelesen und geschrieben werden. Der Inline Scope wiederum wird benutzt, um Escape Analysis zu berechnen und z.B. erst gar keine Objekte zu alloziieren und Variablen zu schreiben.

Bei "volatile" muss die JVM nach dem aktuellen Java Memory Model (eingeführt mit Java 5) bei Schreiboperationen der volatile Variable den Wert in ihre eigentliche Speicheradresse im Hauptspeicher schreiben und Leseoperationen müssen den Wert aus der eigentlichen Speicheradresse im Hauptspeicher lesen. Zusätzlich wird noch sichergestellt, dass kein Reordering/Umsortieren der Schreib- und Leseoperationen vor und nach dem Lesen/Schreiben der volatile Variablen passieren. Das bedeutet auch, dass etwaige anderen nicht-volatile Zugriffe, die vor dem Zugriff auf die volatile-Variable stattfanden auch automatisch für andere Threads sichtbar werden (seit Java 5).
Das heißt, die JVM muss hier genauso wie bei synchronized sicherstellen, dass alle in dem Inline Scope zwischengespeicherten Variablen auch korrekt in den Hauptspeicher rausgeschrieben wurden.
 

httpdigest

Top Contributor
Kleiner Nachtrag: http://gee.cs.oswego.edu/dl/jmm/cookbook.html
Dieses Dokument wurde auch in der SO Antwort https://stackoverflow.com/questions/1850270/memory-effects-of-synchronization-in-java#answer-1863612 erwähnt, und es ist eine hervorragende (alte aber immer noch relevante) Dokumentation, was eine JVM tun muss, um das Java Memory Model (für diverse genannte Prozessor-Architekturen) einzuhalten. Die Seite unterstreicht nochmal, dass es dabei hauptsächlich um Regeln für die Umsortierung von emittierten CPU Instruktionen durch die JVM bzw. den JIT-Compiler und durch die CPU selbst geht. Letzteres durch den Einsatz von Memory Barriers.
 

Ähnliche Java Themen

Neue Themen


Oben