Variablen werden in Java in zwei Kategorien unterteilt: lokale Variablen und Klassenvariablen. Lokale Variablen beziehen sich auf Variablen, die innerhalb einer Methode definiert sind, beispielsweise Variablen, die in der Ausführungsmethode definiert sind. Für diese Variablen gibt es kein Problem bei der gemeinsamen Nutzung zwischen Threads. Daher ist keine Datensynchronisierung erforderlich. Klassenvariablen sind in einer Klasse definierte Variablen und ihr Geltungsbereich erstreckt sich auf die gesamte Klasse. Solche Variablen können von mehreren Threads gemeinsam genutzt werden. Daher müssen wir eine Datensynchronisierung für solche Variablen durchführen.
Datensynchronisation bedeutet, dass nur ein Thread gleichzeitig auf synchronisierte Klassenvariablen zugreifen kann. Nachdem der aktuelle Thread auf diese Variablen zugegriffen hat, können andere Threads weiterhin darauf zugreifen. Der hier genannte Zugriff bezieht sich auf den Zugriff mit Schreiboperationen. Wenn es sich bei allen Threads, die auf Klassenvariablen zugreifen, um Leseoperationen handelt, ist eine Datensynchronisation im Allgemeinen nicht erforderlich. Was passiert also, wenn die Datensynchronisierung für gemeinsam genutzte Klassenvariablen nicht durchgeführt wird? Schauen wir uns zunächst an, was mit dem folgenden Code passiert:
Kopieren Sie den Codecode wie folgt:
Pakettest;
Die öffentliche Klasse MyThread erweitert Thread
{
öffentliches statisches int n = 0;
public void run()
{
int m = n;
Ertrag();
m++;
n = m;
}
public static void main(String[] args) löst eine Ausnahme aus
{
MyThread myThread = new MyThread ();
Thread-Threads[] = neuer Thread[100];
for (int i = 0; i < threads.length; i++)
threads[i] = neuer Thread(myThread);
for (int i = 0; i < threads.length; i++)
threads[i].start();
for (int i = 0; i < threads.length; i++)
threads[i].join();
System.out.println("n = " + MyThread.n);
}
}
Die möglichen Ergebnisse der Ausführung des obigen Codes sind wie folgt:
Kopieren Sie den Codecode wie folgt:
n=59
Viele Leser werden von diesem Ergebnis überrascht sein. Dieses Programm startet offensichtlich 100 Threads, und dann erhöht jeder Thread die statische Variable n um 1. Verwenden Sie abschließend die Join-Methode, um alle 100 Threads auszuführen, und geben Sie dann den n-Wert aus. Normalerweise sollte das Ergebnis n = 100 sein. Aber das Ergebnis liegt unter 100.
Tatsächlich sind die „schmutzigen Daten“, die wir oft erwähnen, für dieses Ergebnis verantwortlich. Die yield()-Anweisung in der run-Methode ist der Initiator von „Dirty Data“ (ohne Hinzufügen einer Yield-Anweisung können auch „Dirty Data“ generiert werden, dies ist jedoch nicht so offensichtlich. Nur durch Ändern von 100 in eine größere Zahl wird dies der Fall sein Es kommt häufig vor, dass „schmutzige Daten“ generiert werden. In diesem Beispiel dient der Aufruf von „Yield“ dazu, den Effekt von „schmutzigen Daten“ zu verstärken. Die Funktion der Yield-Methode besteht darin, den Thread anzuhalten, das heißt, den Thread, der die Yield-Methode aufruft, vorübergehend CPU-Ressourcen zu überlassen und der CPU die Möglichkeit zu geben, andere Threads auszuführen. Um zu veranschaulichen, wie dieses Programm „schmutzige Daten“ generiert, nehmen wir an, dass nur zwei Threads erstellt werden: Thread1 und Thread2. Da die Startmethode von Thread1 zuerst aufgerufen wird, wird im Allgemeinen zuerst die Ausführungsmethode von Thread1 ausgeführt. Wenn die Ausführungsmethode von Thread1 zur ersten Zeile ausgeführt wird (int m = n;), wird m der Wert von n zugewiesen. Wenn die Yield-Methode der zweiten Zeile ausgeführt wird, wird die Ausführung von Thread1 vorübergehend gestoppt, und Thread2 beginnt mit der Ausführung, nachdem er die CPU-Ressourcen abgerufen hat (Thread2 befand sich zuvor im Bereitschaftszustand, wenn Thread2 die erste Zeile ausführte). m = n;), da n immer noch 0 ist, wenn Thread1 ausgeführt wird, um nachzugeben, ist der von m in Thread2 erhaltene Wert ebenfalls 0. Dies führt dazu, dass die m-Werte von Thread1 und Thread2 beide 0 erhalten. Nachdem sie die Yield-Methode ausgeführt haben, beginnen sie alle bei 0 und addieren 1. Daher ist der Endwert von n unabhängig davon, wer sie zuerst ausführt, 1, diesem n wird jedoch von Thread1 bzw. Thread2 ein Wert zugewiesen. Jemand könnte fragen: Wenn es nur n++ gibt, werden dann „schmutzige Daten“ generiert? Die Antwort ist ja. N++ ist also nur eine Anweisung. Wie kann man also die CPU während der Ausführung an andere Threads übergeben? Tatsächlich ist dies nur ein oberflächliches Phänomen, nachdem n++ vom Java-Compiler in eine Zwischensprache (auch Bytecode genannt) kompiliert wurde. Schauen wir uns an, in welche Java-Zwischensprache der folgende Java-Code kompiliert wird.
Kopieren Sie den Codecode wie folgt:
public void run()
{
n++;
}
Kompilierter Zwischensprachencode
Kopieren Sie den Codecode wie folgt:
public void run()
{
aload_0
Dup
getfield
iconst_1
iadd
Putfield
zurückkehren
}
Sie können sehen, dass die Ausführungsmethode nur n++-Anweisungen enthält, nach der Kompilierung jedoch 7 Zwischensprachenanweisungen vorhanden sind. Wir müssen nicht wissen, welche Funktionen diese Anweisungen haben. Schauen Sie sich einfach die Anweisungen in den Zeilen 005, 007 und 008 an. Zeile 005 ist getfield. Wir wissen, dass wir einen bestimmten Wert erhalten möchten. Da es hier nur ein n gibt, besteht kein Zweifel, dass wir den Wert von n erhalten möchten. Es ist nicht schwer zu erraten, dass iadd in Zeile 007 dazu dient, 1 zum erhaltenen n-Wert zu addieren. Ich denke, Sie haben vielleicht die Bedeutung von putfield in Zeile 008 erraten. Es ist für die Aktualisierung von n verantwortlich, nachdem 1 wieder zur Klassenvariablen n hinzugefügt wurde. Apropos, Sie haben vielleicht immer noch Zweifel, dass es bei der Ausführung von n++ ausreicht, nur n um 1 zu addieren. Warum ist das so mühsam? Tatsächlich handelt es sich dabei um ein Problem mit dem Java-Speichermodell.
Das Speichermodell von Java ist in einen Hauptspeicherbereich und einen Arbeitsspeicherbereich unterteilt. Der Hauptspeicherbereich speichert alle Instanzen in Java. Das heißt, nachdem wir mit new ein Objekt erstellt haben, werden das Objekt und seine internen Methoden, Variablen usw. in diesem Bereich gespeichert, und n in der MyThread-Klasse wird in diesem Bereich gespeichert. Der Hauptspeicher kann von allen Threads gemeinsam genutzt werden. Der Arbeitsspeicherbereich ist der Thread-Stapel, über den wir zuvor gesprochen haben. In diesem Bereich werden die in der Ausführungsmethode definierten Variablen und die von der Ausführungsmethode aufgerufenen Methoden gespeichert, dh Methodenvariablen. Wenn ein Thread die Variablen im Hauptspeicherbereich ändern möchte, ändert er diese Variablen nicht direkt, sondern kopiert sie in den Arbeitsspeicherbereich des aktuellen Threads. Nach der Änderung wird der Variablenwert mit dem entsprechenden Wert überschrieben im Hauptspeicherbereich.
Nachdem man das Speichermodell von Java verstanden hat, ist es nicht schwer zu verstehen, warum n++ keine atomare Operation ist. Es muss einen Prozess des Kopierens, Hinzufügens von 1 und Überschreiben durchlaufen. Dieser Prozess ähnelt dem in der MyThread-Klasse simulierten. Wie Sie sich vorstellen können, tritt eine Situation auf, die dem Ausführungsergebnis der MyThread-Klasse ähnelt, wenn Thread1 bei der Ausführung von getfield aus irgendeinem Grund unterbrochen wird. Um dieses Problem vollständig zu lösen, müssen wir eine Methode zum Synchronisieren von n verwenden, dh nur ein Thread kann n gleichzeitig ausführen, was auch als atomare Operation für n bezeichnet wird.