Java-Multithreading - Verbinden eines CPU-Threads und eines volatilen Schlüsselworts

8

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:

  • Ich füge etwas Druck in die Worker-Threads ein (wie in den Kommentaren), was wahrscheinlich dazu führt, dass die Worker-Threads irgendwann die CPU aufgeben oder
  • Wenn ich das Flag boolean stop als volatile markiere, wird der Schreibvorgang sofort ausgelöst von Worker-Threads oder
  • gesehen werden
  • Wenn ich den Counter 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%     
Yossi Vainshtein 03.04.2017, 07:25
quelle

4 Antworten

8
  

Es scheint, dass der Hauptthread in t1.join() line festgefahren ist, obwohl beide Worker-Threads wegen der stop = 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

%Vor%

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.

    
Marko Topolnik 03.04.2017, 08:54
quelle
2

Das könnten die möglichen Gründe sein:

  • Das Markieren von "stop" als flüchtig verhindert, dass der Wert in den Worker-Thread-Zuständen (z. B. Registern) zwischengespeichert wird
  • Das Markieren von "t" als flüchtig stellt sicher, dass der aktualisierte Wert gelesen wird, wenn auf "t" zugegriffen wird. Dieses Verhalten kann jedoch auch den aktualisierten Wert von "Stopp" erhalten, wenn JVM diese Variablen nahe beieinander angeordnet hat, sodass sie in derselben "Cache-Zeile" gelesen und gespeichert werden können. Versuchen Sie, @Contened Annotation hinzuzufügen, um zu sehen, ob das Verhalten auch in diesem Fall bestehen bleibt. Weitere Informationen finden Sie unter: Ссылка , < a href="https://mechanical-sympathy.blogspot.am/2011/07/false-sharing.html"> Ссылка , Ссылка
  • Das Aufrufen von "System.out.println ()" führt tatsächlich einen Systemaufruf durch, wodurch ein Übergang zum "nativen Aufrufstack" stattfindet, der wahrscheinlich auch den "Prozessorcache" zur Bereinigung veranlasst. Was muss eine JVM wann machen? eine native Methode aufrufen?

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.

    
Ruben 03.04.2017 08:11
quelle
0

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.

    
Andrew Williamson 03.04.2017 08:15
quelle
0

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 .

Für Teile mit nichtflüchtigen 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.

Siehe auch

Java-Sprache Spezifikation: 8.3.1.4. flüchtige Felder

Ein Artikel über das Java Memory Model, vom Autor von Java Theorie und Praxis

    
Pavlus 03.04.2017 08:31
quelle

Tags und Links