Werden statische Variablen von Threads gemeinsam genutzt?

95

Mein Lehrer in einer Java-Klasse der Oberstufe zum Thema Threading sagte etwas, dessen ich mir nicht sicher war.

Er erklärte, dass der folgende Code die readyVariable nicht unbedingt aktualisieren würde . Ihm zufolge teilen sich die beiden Threads nicht unbedingt die statische Variable, insbesondere in dem Fall, dass jeder Thread (Hauptthread versus ReaderThread) auf einem eigenen Prozessor ausgeführt wird und daher nicht dieselben Register / Cache / etc und eine CPU gemeinsam nutzt wird den anderen nicht aktualisieren.

Im Wesentlichen sagte er, es sei möglich, dass readyim Haupt-Thread aktualisiert wird, aber NICHT im ReaderThread, so dass sich ReaderThreadeine Endlosschleife ergibt.

Er behauptete auch, es sei möglich, dass das Programm drucke 0oder 42. Ich verstehe, wie 42gedruckt werden könnte, aber nicht 0. Er erwähnte, dass dies der Fall sein würde, wenn die numberVariable auf den Standardwert gesetzt wird.

Ich dachte, es ist vielleicht nicht garantiert, dass die statische Variable zwischen den Threads aktualisiert wird, aber das kommt mir für Java sehr seltsam vor. Behebt das readyVolatilisieren dieses Problem?

Er zeigte diesen Code:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
quelle
Die Sichtbarkeit nicht lokaler Variablen hängt nicht davon ab, ob es sich um statische Variablen, Objektfelder oder Array-Elemente handelt. Sie haben alle die gleichen Überlegungen. (Mit dem Problem, dass Array-Elemente nicht flüchtig gemacht werden können.)
Paŭlo Ebermann
1
Fragen Sie Ihren Lehrer, welche Art von Architektur er für möglich hält, um '0' zu sehen. Theoretisch hat er jedoch recht.
Bests
4
@bestsss Das Stellen dieser Art von Frage würde dem Lehrer zeigen, dass er den ganzen Punkt dessen, was er sagte, verpasst hatte. Der Punkt ist, dass kompetente Programmierer verstehen, was garantiert ist und was nicht und sich nicht auf Dinge verlassen, die nicht garantiert sind, zumindest nicht ohne genau zu verstehen, was nicht garantiert ist und warum.
David Schwartz
Sie werden von allen gemeinsam genutzt, die von demselben Klassenladeprogramm geladen werden. Einschließlich Fäden.
Marquis von Lorne
Ihr Lehrer (und die akzeptierte Antwort) haben 100% Recht, aber ich werde erwähnen, dass es selten vorkommt - dies ist die Art von Problem, das sich jahrelang verbirgt und sich nur zeigt, wenn es am schädlichsten wäre. Selbst kurze Tests, die versuchen, das Problem aufzudecken, tun in der Regel so, als ob alles in Ordnung wäre (wahrscheinlich, weil die JVM keine Zeit hat, viel zu optimieren). Daher ist es ein wirklich gutes Problem, sich dessen bewusst zu sein.
Bill K

Antworten:

75

Statische Variablen haben nichts Besonderes, wenn es um Sichtbarkeit geht. Wenn auf sie zugegriffen werden kann, kann jeder Thread auf sie zugreifen, sodass es wahrscheinlicher ist, dass Parallelitätsprobleme auftreten, da sie stärker gefährdet sind.

Das Speichermodell der JVM weist ein Sichtbarkeitsproblem auf. In diesem Artikel geht es um das Speichermodell und darum, wie Schreibvorgänge für Threads sichtbar werden . Sie können sich nicht darauf verlassen, dass Änderungen, die ein Thread macht, für andere Threads rechtzeitig sichtbar werden (tatsächlich ist die JVM nicht verpflichtet, diese Änderungen in jedem Zeitraum für Sie sichtbar zu machen), es sei denn, Sie stellen eine Beziehung her, bevor dies geschieht .

Hier ist ein Zitat von diesem Link (im Kommentar von Jed Wesley-Smith):

Kapitel 17 der Java-Sprachspezifikation definiert die Vor-Vor-Beziehung für Speicheroperationen wie Lesen und Schreiben von gemeinsam genutzten Variablen. Die Ergebnisse eines Schreibvorgangs durch einen Thread sind für einen Lesevorgang durch einen anderen Thread garantiert nur dann sichtbar, wenn der Schreibvorgang vor dem Lesevorgang ausgeführt wird. Die synchronisierten und flüchtigen Konstrukte sowie die Methoden Thread.start () und Thread.join () können Vorher-Beziehungen bilden. Bestimmtes:

  • Jede Aktion in einem Thread findet statt - vor jeder Aktion in diesem Thread, die später in der Reihenfolge des Programms erfolgt.

  • Ein Entsperren (synchronisierter Block oder Methodenausgang) eines Monitors erfolgt vor jeder nachfolgenden Sperre (synchronisierter Block oder Methodeneintrag) desselben Monitors. Und da die Beziehung "Vorher passiert" transitiv ist, werden alle Aktionen eines Threads vor dem Entsperren ausgeführt - vor allen Aktionen nach dem Sperren eines Threads, der diesen Monitor überwacht.

  • Ein Schreibvorgang in ein flüchtiges Feld erfolgt vor jedem nachfolgenden Lesen desselben Feldes. Das Schreiben und Lesen flüchtiger Felder hat ähnliche Auswirkungen auf die Speicherkonsistenz wie das Betreten und Verlassen von Monitoren, führt jedoch nicht zu einer gegenseitigen Ausschlusssperre.

  • Ein Aufruf zum Starten eines Threads erfolgt vor einer Aktion im gestarteten Thread.

  • Alle Aktionen in einem Thread werden ausgeführt, bevor ein anderer Thread erfolgreich von einem Join in diesem Thread zurückgegeben wird.

Nathan Hughes
quelle
3
In der Praxis sind "rechtzeitig" und "immer" synonym. Es wäre sehr gut möglich, dass der obige Code niemals beendet wird.
Baum
4
Dies zeigt auch ein anderes Anti-Muster. Verwenden Sie flüchtig nicht, um mehr als einen gemeinsamen Status zu schützen. Hier sind number und ready zwei Statuselemente. Um beide konsistent zu aktualisieren / zu lesen, benötigen Sie eine tatsächliche Synchronisierung.
Baum
5
Der Teil darüber, irgendwann sichtbar zu werden, ist falsch. Ohne eine explizite Beziehung, die vorher passiert, gibt es keine Garantie dafür, dass ein Schreibvorgang jemals von einem anderen Thread gesehen wird, da die JIT durchaus das Recht hat, das Lesen in ein Register zu schreiben, und Sie dann niemals ein Update sehen werden. Jede eventuelle Belastung ist Glück und sollte nicht als verlässlich angesehen werden.
Jed Wesley-Smith
2
"Es sei denn, Sie verwenden das flüchtige Schlüsselwort oder synchronisieren." sollte lauten "es sei denn, es besteht eine relevante Beziehung zwischen dem Verfasser und dem Leser" und diesem Link: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith
2
@bestsss gut entdeckt. Leider ist ThreadGroup in vielerlei Hinsicht kaputt.
Jed Wesley-Smith
37

Er sprach von Sichtbarkeit und nicht zu wörtlich zu nehmen.

Statische Variablen werden zwar von Threads gemeinsam genutzt, aber die in einem Thread vorgenommenen Änderungen sind möglicherweise nicht sofort für einen anderen Thread sichtbar, sodass es den Anschein hat, als gäbe es zwei Kopien der Variablen.

Dieser Artikel enthält eine Ansicht, die mit der Darstellung der Informationen übereinstimmt:

Zunächst müssen Sie etwas über das Java-Speichermodell verstehen. Ich habe über die Jahre ein bisschen Mühe gehabt, es kurz und gut zu erklären. Ab heute kann ich es mir am besten beschreiben, wenn Sie es sich so vorstellen:

  • Jeder Thread in Java findet in einem separaten Speicherbereich statt (dies ist eindeutig falsch, nehmen Sie also Kontakt mit mir auf).

  • Sie müssen spezielle Mechanismen verwenden, um sicherzustellen, dass die Kommunikation zwischen diesen Threads wie bei einem Nachrichtenübermittlungssystem erfolgt.

  • Speicherschreibvorgänge, die in einem Thread stattfinden, können "durchlecken" und von einem anderen Thread gesehen werden, dies ist jedoch keineswegs garantiert. Ohne explizite Kommunikation können Sie nicht garantieren, welche Schreibvorgänge von anderen Threads gesehen werden oder in welcher Reihenfolge sie gesehen werden.

...

Gewindemodell

Aber auch dies ist einfach ein mentales Modell, um über Threading und Volatilität nachzudenken, nicht wörtlich, wie die JVM funktioniert.

Bert F.
quelle
12

Grundsätzlich ist es wahr, aber tatsächlich ist das Problem komplexer. Die Sichtbarkeit gemeinsam genutzter Daten kann nicht nur durch CPU-Caches beeinträchtigt werden, sondern auch durch die fehlerhafte Ausführung von Anweisungen.

Daher definiert Java ein Speichermodell , das angibt, unter welchen Umständen Threads den konsistenten Status der gemeinsam genutzten Daten sehen können.

In Ihrem speziellen Fall volatilegarantiert das Hinzufügen die Sichtbarkeit.

axtavt
quelle
8

Sie werden natürlich in dem Sinne "geteilt", dass sie beide auf dieselbe Variable verweisen, aber sie sehen nicht unbedingt die Aktualisierungen des anderen. Dies gilt für jede Variable, nicht nur für statische.

Und theoretisch können Schreibvorgänge, die von einem anderen Thread ausgeführt werden, in einer anderen Reihenfolge erscheinen, es sei denn, die Variablen werden deklariert volatileoder die Schreibvorgänge werden explizit synchronisiert.

biziclop
quelle
4

Innerhalb eines einzelnen Klassenladeprogramms werden statische Felder immer gemeinsam genutzt. Um Daten explizit auf Threads zu beschränken, möchten Sie eine Funktion wie verwenden ThreadLocal.

Kirk Woll
quelle
2

Beim Initialisieren einer statischen primitiven Typvariablen weist Java Default einen Wert für statische Variablen zu

public static int i ;

Wenn Sie die Variable wie folgt definieren, ist der Standardwert von i = 0; Aus diesem Grund besteht die Möglichkeit, dass Sie 0 erhalten. Dann aktualisiert der Hauptthread den Wert von boolean ready to true. Da ready eine statische Variable ist, verweisen der Hauptthread und der andere Thread auf dieselbe Speicheradresse, sodass sich die Ready-Variable ändert. Der sekundäre Thread verlässt also die while-Schleife und druckt den Wert. Beim Drucken ist der initialisierte Wert der Zahl 0. Wenn der Thread-Prozess während der Schleife vor der Variablen für die Aktualisierung der Haupt-Thread-Nummer durchlaufen wurde. dann besteht die Möglichkeit 0 zu drucken

NuOne
quelle
-2

@dontocsata du kannst zu deinem Lehrer zurückkehren und ihn ein wenig schulen :)

wenige Notizen aus der realen Welt und unabhängig davon, was Sie sehen oder erzählt werden. Bitte beachten Sie, dass sich die folgenden Wörter in der angegebenen Reihenfolge auf diesen speziellen Fall beziehen.

Die folgenden 2 Variablen befinden sich in praktisch jeder bekannten Architektur in derselben Cache-Zeile.

private static boolean ready;  
private static int number;  

Thread.exit(Haupt-Thread) wird garantiert beendet und exitverursacht aufgrund der Entfernung des Thread-Gruppen-Threads (und vieler anderer Probleme) garantiert einen Speicherzaun. (Es ist ein synchronisierter Aufruf, und ich sehe keine einzige Möglichkeit, ohne den Synchronisierungsteil implementiert zu werden, da die ThreadGroup ebenfalls beendet werden muss, wenn keine Daemon-Threads mehr vorhanden sind usw.).

Der gestartete Thread ReaderThreadwird den Prozess am Leben erhalten, da es sich nicht um einen Daemon handelt! Somit wird readyund numberzusammen gespült (oder die Nummer vorher, wenn ein Kontextwechsel auftritt) und es gibt in diesem Fall keinen wirklichen Grund für eine Neuordnung, zumindest kann ich mir nicht einmal einen vorstellen. Sie werden etwas wirklich Seltsames brauchen, um etwas anderes zu sehen 42. Ich gehe wieder davon aus, dass sich beide statischen Variablen in derselben Cache-Zeile befinden. Ich kann mir einfach keine 4 Byte lange Cache-Zeile oder eine JVM vorstellen, die sie nicht in einem durchgehenden Bereich (Cache-Zeile) zuweist.

bestsss
quelle
3
@bestsss Während dies heute alles zutrifft, stützt es sich auf die aktuelle JVM-Implementierung und Hardwarearchitektur, nicht auf die Semantik des Programms. Dies bedeutet, dass das Programm immer noch kaputt ist, obwohl es möglicherweise funktioniert. Es ist leicht, eine triviale Variante dieses Beispiels zu finden, die auf die angegebenen Arten tatsächlich fehlschlägt.
Jed Wesley-Smith
1
Ich habe gesagt, dass es nicht der Spezifikation entspricht, aber als Lehrer zumindest ein geeignetes Beispiel finden, das bei einigen Warenarchitekturen tatsächlich fehlschlagen kann, so dass das Beispiel irgendwie real ist.
Bests
6
Möglicherweise der schlechteste Rat zum Schreiben von thread-sicherem Code, den ich gesehen habe.
Lawrence Dol
4
@Bestsss: Die einfache Antwort auf Ihre Frage lautet einfach: "Code für die Spezifikation und das Dokument, nicht für die Nebenwirkungen Ihres speziellen Systems oder Ihrer Implementierung". Dies ist besonders wichtig bei einer Plattform für virtuelle Maschinen, die so konzipiert ist, dass sie unabhängig von der zugrunde liegenden Hardware ist.
Lawrence Dol
1
@Bestsss: Der Lehrer weist darauf hin, dass (a) der Code beim Testen möglicherweise funktioniert und (b) der Code fehlerhaft ist, da er vom Hardwarebetrieb und nicht von den Garantien der Spezifikation abhängt. Der Punkt ist, dass es so aussieht, als wäre es in Ordnung, aber es ist nicht in Ordnung.
Lawrence Dol