CodeGym /Java-Blog /Random-DE /Threads verwalten. Das Schlüsselwort volatile und die Met...
Autor
Volodymyr Portianko
Java Engineer at Playtika

Threads verwalten. Das Schlüsselwort volatile und die Methode yield()

Veröffentlicht in der Gruppe Random-DE
Hallo! Wir setzen unsere Studie zum Multithreading fort. Heute lernen wir das volatileSchlüsselwort und die yield()Methode kennen. Lasst uns eintauchen :)

Das Schlüsselwort volatile

Beim Erstellen von Multithread-Anwendungen können zwei schwerwiegende Probleme auftreten. Erstens können verschiedene Threads, wenn eine Multithread-Anwendung ausgeführt wird, die Werte von Variablen zwischenspeichern (wir haben darüber bereits in der Lektion mit dem Titel „Volatil verwenden“ gesprochen ). Es kann vorkommen, dass ein Thread den Wert einer Variablen ändert, ein zweiter Thread die Änderung jedoch nicht sieht, weil er mit seiner zwischengespeicherten Kopie der Variablen arbeitet. Natürlich können die Folgen schwerwiegend sein. Angenommen, es handelt sich nicht um irgendeine Variable, sondern um den Kontostand Ihres Bankkontos, der plötzlich zufällig auf und ab springt :) Das hört sich nicht nach Spaß an, oder? Zweitens, in Java, Operationen zum Lesen und Schreiben aller primitiven Typen,longdouble, sind atomar. Wenn Sie beispielsweise den Wert einer intVariablen in einem Thread ändern und in einem anderen Thread den Wert der Variablen lesen, erhalten Sie entweder ihren alten Wert oder den neuen, also den Wert, der sich aus der Änderung ergibt in Thread 1. Es gibt keine „Zwischenwerte“. Dies funktioniert jedoch nicht mit longs und doubles. Warum? Aufgrund der plattformübergreifenden Unterstützung. Erinnern Sie sich, dass wir in den Anfangsstufen gesagt haben, dass das Leitprinzip von Java lautet: „Einmal schreiben, überall ausführen“? Das bedeutet plattformübergreifende Unterstützung. Mit anderen Worten: Eine Java-Anwendung läuft auf den unterschiedlichsten Plattformen. Beispielsweise auf Windows-Betriebssystemen, verschiedenen Versionen von Linux oder MacOS. Es läuft bei allen ohne Probleme. Mit einem Gewicht von 64 Bit,longdoublesind die „schwersten“ Grundelemente in Java. Und bestimmte 32-Bit-Plattformen implementieren einfach kein atomares Lesen und Schreiben von 64-Bit-Variablen. Das Lesen und Schreiben solcher Variablen erfolgt in zwei Vorgängen. Zuerst werden die ersten 32 Bit in die Variable geschrieben und dann werden weitere 32 Bit geschrieben. Infolgedessen kann ein Problem auftreten. Ein Thread schreibt einen 64-Bit-Wert in eine XVariable und tut dies in zwei Vorgängen. Gleichzeitig versucht ein zweiter Thread, den Wert der Variablen zu lesen, und zwar zwischen diesen beiden Vorgängen – wenn die ersten 32 Bit geschrieben wurden, die zweiten 32 Bit jedoch nicht. Infolgedessen wird ein falscher Zwischenwert gelesen, und es liegt ein Fehler vor. Wenn wir beispielsweise auf einer solchen Plattform versuchen, die Nummer auf 9223372036854775809 zu schreiben zu einer Variablen hinzufügt, belegt sie 64 Bit. In binärer Form sieht es so aus: 100000000000000000000000000000000000000000000000000000000000000001 Der erste Thread beginnt, die Zahl in die Variable zu schreiben. Zuerst werden die ersten 32 Bits geschrieben (1000000000000000000000000000000) und dann die zweiten 32 Bits (000000000000000000000000000001). Und der zweite Thread kann zwischen diesen Vorgängen eingeklemmt werden und den Zwischenwert der Variablen lesen (10000000000000000000000000000000), also die ersten 32 Bits, die bereits geschrieben wurden. Im Dezimalsystem beträgt diese Zahl 2.147.483.648. Mit anderen Worten, wir wollten nur die Zahl 9223372036854775809 in eine Variable schreiben, aber aufgrund der Tatsache, dass diese Operation auf einigen Plattformen nicht atomar ist, haben wir die böse Zahl 2.147.483.648, die aus dem Nichts kam und einen unbekannten Effekt haben wird Programm. Der zweite Thread las einfach den Wert der Variablen, bevor der Schreibvorgang abgeschlossen war, dh der Thread sah die ersten 32 Bits, aber nicht die zweiten 32 Bits. Natürlich sind diese Probleme gestern nicht aufgetreten. Java löst sie mit einem einzigen Schlüsselwort: volatile. Wenn wir das verwendenvolatileSchlüsselwort beim Deklarieren einer Variablen in unserem Programm ...

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…es bedeutet, dass:
  1. Es wird immer atomar gelesen und geschrieben. Auch wenn es eine 64-Bit- doubleoder long.
  2. Die Java-Maschine speichert es nicht zwischen. Es kommt also nicht zu einer Situation, in der 10 Threads mit ihren eigenen lokalen Kopien arbeiten.
Somit werden zwei sehr ernste Probleme mit nur einem Wort gelöst :)

Die yield()-Methode

Wir haben bereits viele ThreadMethoden der Klasse besprochen, aber es gibt eine wichtige, die für Sie neu sein wird. Es ist die yield()Methode . Und es tut genau das, was sein Name vermuten lässt! Threads verwalten.  Das Schlüsselwort volatile und die yield()-Methode – 2Wenn wir die yieldMethode für einen Thread aufrufen, kommuniziert sie tatsächlich mit den anderen Threads: „Hey, Leute.“ Ich habe es nicht besonders eilig, irgendwohin zu gehen. Wenn es also wichtig ist, dass einer von euch Prozessorzeit bekommt, dann nehmt sie – ich kann warten.“ Hier ist ein einfaches Beispiel, wie das funktioniert:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Wir erstellen und starten nacheinander drei Threads: Thread-0, Thread-1, und Thread-2. Thread-0startet als Erster und gibt sofort den anderen nach. Dann Thread-1wird begonnen und es gibt auch Früchte. Dann Thread-2wird gestartet, was auch nachgibt. Wir haben keine Threads mehr, und nachdem Thread-2der Thread-Scheduler den letzten Platz freigegeben hat, sagt er: „Hmm, es gibt keine neuen Threads mehr.“ Wen haben wir in der Warteschlange? Wer hat zuvor seinen Platz eingeräumt Thread-2? Es scheint, dass es so war Thread-1. „Okay, das heißt, wir lassen es laufen.“ Thread-1schließt seine Arbeit ab und dann setzt der Thread-Scheduler seine Koordination fort: „Okay, Thread-1fertig.“ Haben wir noch jemanden in der Warteschlange?‘. Thread-0 befindet sich in der Warteschlange: Er hat kurz zuvor seinen Platz aufgegebenThread-1. Nun kommt es an die Reihe und läuft zu Ende. Dann beendet der Scheduler die Koordinierung der Threads: „Okay, Thread-2Sie haben anderen Threads nachgegeben, und sie sind jetzt alle fertig.“ Du warst der Letzte, der nachgegeben hat, also bist du jetzt an der Reihe. Läuft dann Thread-2bis zum Abschluss. Die Konsolenausgabe sieht folgendermaßen aus: Thread-0 übergibt seinen Platz an andere Thread-1 übergibt seinen Platz an andere Thread-2 übergibt seinen Platz an andere Thread-1 hat die Ausführung abgeschlossen. Thread-0 ist mit der Ausführung fertig. Die Ausführung von Thread-2 ist abgeschlossen. Natürlich könnte der Thread-Scheduler die Threads in einer anderen Reihenfolge starten (z. B. 2-1-0 statt 0-1-2), aber das Prinzip bleibt dasselbe.

Passiert vor den Regeln

Das Letzte, worauf wir heute eingehen werden, ist das Konzept „ passiert vorher “. Wie Sie bereits wissen, erledigt der Thread-Scheduler in Java den Großteil der Arbeit, die mit der Zuweisung von Zeit und Ressourcen an Threads zur Ausführung ihrer Aufgaben verbunden ist. Sie haben auch immer wieder gesehen, wie Threads in einer zufälligen Reihenfolge ausgeführt werden, die normalerweise nicht vorhersehbar ist. Und im Allgemeinen sieht die Multithread-Programmierung nach der „sequentiellen“ Programmierung, die wir zuvor durchgeführt haben, wie etwas Zufälliges aus. Sie sind bereits davon überzeugt, dass Sie eine Vielzahl von Methoden verwenden können, um den Ablauf eines Multithread-Programms zu steuern. Aber Multithreading in Java hat noch eine weitere Säule – die 4 „ Passiert-vorher “-Regeln. Diese Regeln zu verstehen ist ganz einfach. Stellen Sie sich vor, wir haben zwei Threads – AundB. Jeder dieser Threads kann Operationen ausführen 1und 2. Wenn wir in jeder Regel sagen „ A passiert vor B “, meinen wir, dass alle vom Thread Avor der Operation vorgenommenen Änderungen 1und die aus dieser Operation resultierenden Änderungen für den Thread sichtbar sind, Bwenn die Operation 2ausgeführt wird und danach. 2Jede Regel garantiert, dass beim Schreiben eines Multithread-Programms bestimmte Ereignisse zu 100 % vor anderen auftreten und dass der Thread zum Zeitpunkt des Vorgangs Bimmer über die Änderungen informiert ist, die der Thread Awährend des Vorgangs vorgenommen hat 1. Lassen Sie uns sie überprüfen.

Regel 1.

Das Freigeben eines Mutex erfolgt, bevor derselbe Monitor von einem anderen Thread erfasst wird. Ich denke, du verstehst hier alles. Wenn der Mutex eines Objekts oder einer Klasse von einem Thread erfasst wird, beispielsweise von thread , kann ihn Aein anderer Thread (thread B) nicht gleichzeitig abrufen. Es muss warten, bis der Mutex freigegeben wird.

Regel 2.

Die Thread.start()Methode findet vorher statt Thread.run() . Auch hier gibt es nichts Schwieriges. Sie wissen bereits, dass Sie die Methode im Thread run()aufrufen müssen, um mit der Ausführung des Codes innerhalb der Methode zu beginnen. start()Insbesondere die Startmethode, nicht die run()Methode selbst! Diese Regel stellt sicher, dass die Werte aller vor Thread.start()dem Aufruf festgelegten Variablen innerhalb der Methode sichtbar sind, run()sobald sie beginnt.

Regel 3.

Das Ende der run()Methode erfolgt vor der Rückkehr von der join()Methode. Kehren wir zu unseren beiden Threads zurück: Aund B. Wir rufen die join()Methode auf, damit der Thread Bgarantiert auf den Abschluss des Threads wartet, Abevor er seine Arbeit erledigt. Dies bedeutet, dass die Methode des A-Objekts run()garantiert bis zum Ende ausgeführt wird. Und alle Änderungen an Daten, die in der run()Thread-Methode vorgenommen werden A, sind hundertprozentig garantiert im Thread sichtbar, Bsobald sie fertig sind und darauf warten, dass der Thread Aseine Arbeit beendet, damit er mit seiner eigenen Arbeit beginnen kann.

Regel 4.

Das Schreiben in eine volatileVariable erfolgt vor dem Lesen derselben Variablen. Wenn wir das Schlüsselwort verwenden volatile, erhalten wir eigentlich immer den aktuellen Wert. Auch mit einem longoder double(wir haben vorhin über Probleme gesprochen, die hier auftreten können). Wie Sie bereits wissen, sind in einigen Threads vorgenommene Änderungen für andere Threads nicht immer sichtbar. Aber natürlich gibt es sehr häufig Situationen, in denen uns ein solches Verhalten nicht passt. Angenommen, wir weisen einer Variablen im Thread einen Wert zu A:

int z;

….

z = 555;
Wenn unser BThread den Wert der zVariablen auf der Konsole anzeigen sollte, könnte er leicht 0 anzeigen, da er den zugewiesenen Wert nicht kennt. Aber Regel 4 garantiert, dass, wenn wir die zVariable als deklarieren volatile, Änderungen an ihrem Wert in einem Thread immer in einem anderen Thread sichtbar sind. Wenn wir das Wort volatilezum vorherigen Code hinzufügen ...

volatile int z;

….

z = 555;
... dann verhindern wir die Situation, in der der Thread Bmöglicherweise 0 anzeigt. Das Schreiben in volatileVariablen erfolgt vor dem Lesen aus ihnen.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION