Bei der gleichzeitigen Verarbeitung von Java-Threads herrscht derzeit große Verwirrung über die Verwendung des Schlüsselworts volatile. Es wird angenommen, dass bei der gleichzeitigen Verarbeitung mehrerer Threads alles in Ordnung ist.
Die Java-Sprache unterstützt Multithreading. Um das Problem der Thread-Parallelität zu lösen, werden in der Sprache die Mechanismen synchronisierter Blöcke und flüchtiger Schlüsselwörter eingeführt.
synchronisiert
Jeder ist mit synchronisierten Blöcken vertraut, die über das synchronisierte Schlüsselwort implementiert werden. Durch das Hinzufügen von synchronisierten und Blockanweisungen kann beim Zugriff mehrerer Threads nur ein Thread gleichzeitig synchronisierte modifizierte Methoden oder Codeblöcke verwenden.
flüchtig
Bei Variablen, die mit flüchtig geändert wurden, liest der Thread jedes Mal, wenn er die Variable verwendet, den geänderten Wert der Variablen. Flüchtige Stoffe können leicht zur Durchführung atomarer Operationen missbraucht werden.
Schauen wir uns unten ein Beispiel an. Wir implementieren einen Zähler. Jedes Mal, wenn der Thread startet, wird die Methode counter inc aufgerufen, um einen zum Zähler hinzuzufügen.
Ausführungsumgebung – JDK-Version: jdk1.6.0_31, Speicher: 3G CPU: x86 2.4G
Kopieren Sie den Codecode wie folgt:
öffentlicher Klassenzähler {
öffentliche statische int count = 0;
public static void inc() {
// Hier 1 Millisekunde verzögern, um das Ergebnis deutlich zu machen
versuchen {
Thread.sleep(1);
} Catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//Starten Sie 1000 Threads gleichzeitig, um i++-Berechnungen durchzuführen und die tatsächlichen Ergebnisse anzuzeigen
for (int i = 0; i < 1000; i++) {
neuer Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).Start();
}
//Der Wert hier kann bei jeder Ausführung unterschiedlich sein, möglicherweise 1000
System.out.println("Laufergebnis: Counter.count=" + Counter.count);
}
}
Laufergebnis: Counter.count=995
Das tatsächliche Operationsergebnis kann jedes Mal anders sein: Laufergebnis: Counter.count=995 Es ist ersichtlich, dass Counter.count in einer Multithread-Umgebung nicht erwartet, dass das Ergebnis 1000 ist.
Viele Leute glauben, dass dies ein Multithread-Parallelitätsproblem ist. Um dieses Problem zu vermeiden, müssen wir nur volatile hinzufügen.
Kopieren Sie den Codecode wie folgt:
öffentlicher Klassenzähler {
public volatile static int count = 0;
public static void inc() {
// Hier 1 Millisekunde verzögern, um das Ergebnis deutlich zu machen
versuchen {
Thread.sleep(1);
} Catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//Starten Sie 1000 Threads gleichzeitig, um i++-Berechnungen durchzuführen und die tatsächlichen Ergebnisse anzuzeigen
for (int i = 0; i < 1000; i++) {
neuer Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).Start();
}
//Der Wert hier kann bei jeder Ausführung unterschiedlich sein, möglicherweise 1000
System.out.println("Laufergebnis: Counter.count=" + Counter.count);
}
}
Laufergebnis: Counter.count=992
Das laufende Ergebnis ist immer noch nicht 1000, wie wir erwartet hatten. Lassen Sie uns die Gründe unten analysieren.
Im Artikel Java Garbage Collection wird die Zuweisung von Speicher während der JVM-Laufzeit beschrieben. Einer der Speicherbereiche ist der JVM-Stapel der virtuellen Maschine. Jeder Thread verfügt über einen Thread-Stapel, wenn er ausgeführt wird. Wenn ein Thread auf den Wert eines Objekts zugreift, findet er zunächst über die Objektreferenz den Wert der Variablen, die dem Heap-Speicher entspricht, und lädt dann den spezifischen Wert der Heap-Speichervariablen in den lokalen Speicher des Threads, um eine Kopie davon zu erstellen Danach ändert der Thread nichts mehr mit dem Variablenwert des Objekts im Heap-Speicher, sondern ändert ihn direkt zu einem bestimmten Zeitpunkt nach der Änderung (bevor der Thread beendet wird). Der Wert der Thread-Variablenkopie wird automatisch in die Variable des Objekts im Heap zurückgeschrieben. Auf diese Weise ändert sich der Wert des Objekts im Heap. Das Bild unten
Beschreiben Sie diese Interaktion
Beim Lesen und Laden werden Variablen vom Hauptspeicher in den aktuellen Arbeitsspeicher kopiert
Verwenden und Zuweisen von Ausführungscode und Ändern gemeinsamer Variablenwerte
Speichern und Schreiben von hauptspeicherbezogenen Inhalten mit Arbeitsspeicherdaten
wobei „use“ und „assign“ mehrfach vorkommen können
Diese Vorgänge sind jedoch nicht atomar. Das heißt, wenn die Hauptspeicheranzahlvariable geändert wird, ändert sich der Wert im Thread-Arbeitsspeicher nicht, sodass das berechnete Ergebnis wie erwartet ausfällt das gleiche
Bei flüchtig geänderten Variablen stellt die virtuelle JVM-Maschine nur sicher, dass der vom Hauptspeicher in den Thread-Arbeitsspeicher geladene Wert der neueste ist
Wenn beispielsweise Thread 1 und Thread 2 beim Lese- und Ladevorgang feststellen, dass der Zählwert im Hauptspeicher jeweils 5 beträgt, laden sie den neuesten Wert.
Nachdem die Heap-Zählung von Thread 1 geändert wurde, wird sie in den Hauptspeicher geschrieben und die Zählvariable im Hauptspeicher wird zu 6.
Da Thread 2 bereits Lese- und Ladevorgänge ausgeführt hat, aktualisiert er nach der Ausführung des Vorgangs auch den Wert der Hauptspeicher-Zählervariablen auf 6.
Dies führt dazu, dass nach der rechtzeitigen Änderung zweier Threads mithilfe des Schlüsselworts volatile weiterhin Parallelität besteht.