Nach einigen Vorstellungsgesprächen wollte ich ein kleines Programm schreiben, um zu überprüfen, ob i++
in Java tatsächlich nicht-atomar ist und dass man in der Praxis einige Sperren hinzufügen sollte, um sie zu schützen. Stellt sich heraus, dass Sie sollten, aber das ist nicht die Frage hier.
Also habe ich dieses Programm hier geschrieben, nur um es zu überprüfen.
Die Sache ist, es hängt. Es scheint, dass der Haupt-Thread auf t1.join()
feststeckt
Zeile, obwohl beide Worker-Threads wegen der stop = true
aus der vorherigen Zeile enden sollten.
Ich habe festgestellt, dass das Hängen aufhört, wenn:
boolean stop
als volatile
markiere, wird der Schreibvorgang sofort ausgelöst
von Worker-Threads oder t
als volatile
... ankreuze, habe ich keine Ahnung, was das Unhängen verursacht. Kann jemand erklären, was vor sich geht? Warum sehe ich den Hang und warum hört es in diesen drei Fällen auf?
%Vor%Es scheint, dass der Hauptthread in
t1.join()
line festgefahren ist, obwohl beide Worker-Threads wegen derstop = true
von der vorherigen Zeile enden sollten.
In Abwesenheit von volatile
, Locking oder eines anderen sicheren Veröffentlichungsmechanismus ist die JVM nicht verpflichtet, jemals stop = true
für andere Threads sichtbar zu machen. Speziell auf Ihren Fall angewendet, während Ihr Haupt-Thread für eine Sekunde schläft, optimiert der JIT-Compiler Ihre while (!stop)
hot-Schleife in das Äquivalent von
Diese spezielle Optimierung ist bekannt als "Hochziehen" der Leseaktion außerhalb der Schleife.
Ich habe festgestellt, dass das Hängen aufhört, wenn:
- Ich füge etwas Druck innerhalb der Worker-Threads hinzu (wie in den Kommentaren), was wahrscheinlich dazu führt, dass die Worker-Threads irgendwann CPU
aufgeben
Nein, das liegt daran, dass PrintStream::println
eine synchronisierte Methode ist. Alle bekannten JVMs werden einen Speicherzaun auf CPU-Ebene ausgeben, um die Semantik einer "Akquise" -Aktion sicherzustellen (in diesem Fall Lock-Akquisition), und dies wird ein erneutes Laden der stop
-Variable erzwingen. Dies ist nicht erforderlich für die Spezifikation, nur eine Implementierung Wahl.
- Wenn ich das Flag
boolean stop
als flüchtig markieren würde, würde der Schreibvorgang sofort von Worker-Threads gesehen werden
Die Spezifikation hat tatsächlich keine Anforderungen an die Wanduhrzeit, wenn ein flüchtiger Schreibvorgang für andere Threads sichtbar werden soll, aber in der Praxis versteht es sich, dass er "sehr bald" sichtbar werden muss. Diese Änderung ist also der richtige Weg, um sicherzustellen, dass das Schreiben in stop
sicher veröffentlicht wird und anschließend von anderen Threads, die es lesen, beobachtet wird.
- Wenn ich den Counter
t
als volatile markiere ... dafür habe ich keine Ahnung was das Unhängen verursacht.
Dies sind wiederum die indirekten Auswirkungen dessen, was die JVM tut, um die Semantik von volatile
read zu gewährleisten, was eine andere Art von "acquire" Inter-Thread-Aktion ist.
Zusammenfassend wechselt Ihr Programm, abgesehen von der Änderung, die stop
eine volatile Variable macht, aufgrund der zufälligen Nebeneffekte der zugrundeliegenden JVM-Implementierung, die zur Vereinfachung einige Flushing / Ungültigmachen des Threads durchführt, vom Hängenbleiben zum Vollenden -local Zustand als von der Spezifikation gefordert.
Das könnten die möglichen Gründe sein:
Wenn Sie daran interessiert sind, tiefer in das Thema einzutauchen, dann würde ich vorschlagen, das Buch "Java Concurrency in Practice by Brian Goetz" zu lesen.
Das Markieren einer Variablen als flüchtig ist ein Hinweis für die JVM, die zugehörigen Segmente des Cache zwischen Threads / Kernen zu flush / sync zu machen, wenn diese Variable aktualisiert wird. Das Markieren von stop
als flüchtig hat dann ein besseres Verhalten (aber nicht perfekt, du kannst einige Extra-Ausführungen auf deinen Threads haben, bevor sie das Update sehen).
Das Markieren von t
als flüchtiges Rätsel überlegt mir, warum es funktioniert, kann es sein, dass, weil dies ein so kleines Programm ist, t
und stop
in der gleichen Zeile im Cache sind, also wenn man geleert wird / synchronisierte das andere auch.
System.out.println
ist threadsicher, daher gibt es intern eine Synchronisierung. Dies kann wiederum dazu führen, dass einige Teile des Caches zwischen den Threads synchronisiert werden.
Wenn jemand das hinzufügen kann, bitte, ich würde auch gerne eine ausführlichere Antwort dazu hören.
Es tut tatsächlich, was es gesagt hat - bietet konsistenten Zugriff auf das Feld zwischen mehreren Threads, und Sie können es sehen.
Ohne volatile
-Schlüsselwort ist Multithread-Zugriff auf das Feld nicht garantiert konsistent, Compiler können einige Optimierungen einführen, wie Caching im CPU-Register oder nicht aus dem lokalen Cache des CPU-Kerns in externen Speicher oder gemeinsamen Cache schreiben .
stop
und volatile t
Gemäß JSR-133 Java Memory Model-Spezifikation werden alle Schreibvorgänge (in jedes andere Feld) vor der Aktualisierung des flüchtigen Feldes sichtbar gemacht, sie sind passed-before -Aktionen.
Wenn Sie stop
flag nach dem Inkrementieren von t
setzen, wird es beim nachfolgenden Lesen in der Schleife nicht sichtbar sein, aber das nächste Inkrement ( volatile-write ) wird es sichtbar machen.
Java-Sprache Spezifikation: 8.3.1.4. flüchtige Felder
Ein Artikel über das Java Memory Model, vom Autor von Java Theorie und Praxis
Tags und Links java multithreading volatile