JavaRush /Java-Blog /Random-DE /Flussmanagement. Das Schlüsselwort volatile und die Metho...

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

Veröffentlicht in der Gruppe Random-DE
Hallo! Wir beschäftigen uns weiterhin mit Multithreading und lernen heute ein neues Schlüsselwort kennen – volatile und die yield()-Methode. Lasst uns herausfinden, was es ist :)

Stichwort volatil

Beim Erstellen von Multithread-Anwendungen können zwei schwerwiegende Probleme auftreten. Erstens können während des Betriebs einer Multithread-Anwendung verschiedene Threads die Werte von Variablen zwischenspeichern (wir werden in der Vorlesung „Volatile verwenden“ mehr darüber sprechen ). Es ist möglich, dass ein Thread den Wert einer Variablen geändert hat, der zweite Thread diese Änderung jedoch nicht gesehen hat, da er mit seiner eigenen zwischengespeicherten Kopie der Variablen gearbeitet hat. Natürlich können die Folgen schwerwiegend sein. Stellen Sie sich vor, dass dies nicht nur eine Art „Variable“ ist, sondern beispielsweise der Kontostand Ihrer Bankkarte, der plötzlich zufällig hin und her springt :) Nicht sehr angenehm, oder? Zweitens sind in Java Lese- und Schreiboperationen für Felder aller Typen außer longund doubleatomar. Was ist Atomizität? Nun, wenn Sie zum Beispiel den Wert einer Variablen in einem Thread ändern intund in einem anderen Thread den Wert dieser Variablen lesen, erhalten Sie entweder ihren alten Wert oder einen neuen – den, der sich nach der Änderung herausstellte Thread 1. Möglicherweise werden dort keine „Zwischenoptionen“ angezeigt. Dies funktioniert jedoch nicht mit longund . doubleWarum? Weil es plattformübergreifend ist. Erinnern Sie sich, wie wir in den ersten Levels sagten, dass das Java-Prinzip lautet: „Einmal geschrieben, funktioniert überall“? Dies ist plattformübergreifend. Das heißt, eine Java-Anwendung läuft auf völlig unterschiedlichen Plattformen. Beispielsweise funktioniert diese Anwendung auf Windows-Betriebssystemen, verschiedenen Linux- oder MacOS-Versionen und überall stabil. longund double- die „schwersten“ Grundelemente in Java: Sie wiegen 64 Bit. Und einige 32-Bit-Plattformen implementieren einfach nicht die Atomizität des Lesens und Schreibens von 64-Bit-Variablen. Das Lesen und Schreiben solcher Variablen erfolgt in zwei Vorgängen. Zuerst werden die ersten 32 Bits in die Variable geschrieben, dann weitere 32. Dementsprechend kann es in diesen Fällen zu Problemen kommen. Ein Thread schreibt einen 64-Bit-Wert in eine VariableХ, und er tut es „in zwei Schritten“. Gleichzeitig versucht der zweite Thread, den Wert dieser Variablen zu lesen, und zwar mittendrin, wenn die ersten 32 Bits bereits geschrieben wurden, die zweiten jedoch noch nicht. Infolgedessen wird ein falscher Zwischenwert gelesen und es tritt ein Fehler auf. Wenn wir beispielsweise auf einer solchen Plattform versuchen, eine Zahl in eine Variable zu schreiben – 9223372036854775809 –, belegt diese 64 Bit. In binärer Form sieht es so aus: 100000000000000000000000000000000000000000000000000000000000000001 Der erste Thread beginnt mit dem Schreiben dieser Zahl in eine Variable und schreibt zunächst die ersten 32 Bits: 100000000000000000000000 00000 00000 und dann die zweite 32: 00000000000000000000000000000001 Und in diese Lücke kann sich ein zweiter Thread verkeilen und Lesen Sie den Zwischenwert der Variablen - 10000000000000000000000000000000, die ersten 32 Bits, die bereits geschrieben wurden. Im Dezimalsystem ist diese Zahl gleich 2147483648. Das heißt, 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 „linke“ Zahl 2147483648 erhalten , was wir nicht brauchen, aus dem Nichts. Es ist nicht bekannt, wie sich dies auf den Betrieb des Programms auswirken wird. Der zweite Thread las einfach den Wert der Variablen, bevor er endgültig geschrieben wurde, d. h. er sah die ersten 32 Bit, aber nicht die zweiten 32 Bit. Diese Probleme sind natürlich gestern nicht aufgetreten und werden in Java mit nur einem Schlüsselwort gelöst – volatile . Wenn wir in unserem Programm eine Variable mit dem Wort volatile deklarieren ...
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 64-Bit doubleoder long.
  2. Die Java-Maschine speichert es nicht zwischen. Daher ist die Situation ausgeschlossen, in der 10 Threads mit ihren lokalen Kopien arbeiten.
So werden zwei sehr ernste Probleme in einem Wort gelöst :)

yield()-Methode

Wir haben uns bereits viele Methoden der Klasse angeschaut Thread, aber es gibt eine wichtige, die für Sie neu sein wird. Dies ist die yield()-Methode . Aus dem Englischen übersetzt als „nachgeben“. Und genau das macht die Methode! Flussmanagement.  Das Schlüsselwort volatile und die yield()-Methode – 2Wenn wir die yield-Methode für einen Thread aufrufen, sagt sie tatsächlich zu anderen Threads: „Okay, Leute, ich habe es nicht besonders eilig. Wenn es also für einen von euch wichtig ist, CPU-Zeit zu bekommen, dann nehmt sie, ich bin es.“ nicht dringlich." Hier ist ein einfaches Beispiel dafür, wie es funktioniert:
public class ThreadExample extends Thread {

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

   public void run() {

       System.out.println(Thread.currentThread().getName() + „Anderen den Vortritt lassen“);
       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-1und Thread-2. Thread-0beginnt zuerst und gibt sofort den anderen nach. Danach startet es Thread-1und gibt auch nach. Danach geht es los Thread-2, was ebenfalls minderwertig ist. Wir haben keine Threads mehr und nachdem Thread-2der letzte seinen Platz aufgegeben hat, schaut der Thread-Scheduler: „Es gibt also keine neuen Threads mehr, wen haben wir in der Warteschlange?“ Wer hat seinen Platz zuvor als Letzter aufgegeben Thread-2? Ich denke, es war Thread-1? Okay, also lass es geschehen.“ Thread-1erledigt seine Arbeit bis zum Ende, danach koordiniert der Thread-Scheduler weiter: „Okay, Thread-1 ist abgeschlossen. Haben wir noch jemanden in der Schlange?“ Es gibt Thread-0 in der Warteschlange: Er hat seinen Platz unmittelbar vor Thread-1 aufgegeben. Jetzt ist die Sache an ihn herangetreten und er wird bis zum Ende ausgeführt. Danach beendet der Scheduler die Koordinierung der Threads: „Okay, Thread-2, Sie haben anderen Threads Platz gemacht, sie haben alle bereits funktioniert. Du warst der Letzte, der nachgegeben hat, also bist du jetzt an der Reihe.“ Danach wird Thread-2 vollständig ausgeführt. Die Konsolenausgabe sieht folgendermaßen aus: Thread-0 weicht anderen Thread-1 weicht anderen Thread-2 weicht anderen Thread-1 ist mit der Ausführung fertig. Die Ausführung von Thread-0 ist abgeschlossen. Die Ausführung von Thread-2 ist abgeschlossen. Der Thread-Scheduler kann Threads natürlich in einer anderen Reihenfolge ausführen (z. B. 2-1-0 statt 0-1-2), aber das Prinzip ist dasselbe.

Passiert vor den Regeln

Das Letzte, worauf wir heute eingehen werden, sind die „ passiert vorher “-Prinzipien. Wie Sie bereits wissen, wird in Java der Thread-Scheduler den Großteil der Zeit- und Ressourcenzuweisung an Threads zur Erledigung ihrer Aufgaben erledigen. Sie haben auch mehr als einmal gesehen, wie Threads in willkürlicher Reihenfolge ausgeführt werden, und meistens ist es unmöglich, dies vorherzusagen. Und im Allgemeinen sieht Multithreading nach der „sequentiellen“ Programmierung, die wir zuvor durchgeführt haben, wie eine zufällige Sache aus. Wie Sie bereits gesehen haben, kann der Fortschritt eines Multithread-Programms mit einer ganzen Reihe von Methoden gesteuert werden. Aber darüber hinaus gibt es im Java-Multithreading eine weitere „Insel der Stabilität“ – 4 Regeln namens „ Passiert vor “. Wörtlich aus dem Englischen wird dies mit „passiert vorher“ oder „passiert vorher“ übersetzt. Die Bedeutung dieser Regeln ist recht einfach zu verstehen. Stellen Sie sich vor, wir haben zwei Threads – Aund B. Jeder dieser Threads kann Operationen ausführen 1und 2. Und wenn wir in jeder der Regeln sagen: „ A geschieht vor B “, bedeutet dies, dass alle Änderungen, die der Thread Avor der Operation vorgenommen hat 1, und die Änderungen, die diese Operation mit sich brachte, für den Thread Bzum Zeitpunkt der Ausführung der Operation sichtbar sind 2und nach Durchführung der Operation. Jede dieser Regeln stellt sicher, dass beim Schreiben eines Multithread-Programms einige Ereignisse zu 100 % vor anderen eintreten und dass der Thread zum Zeitpunkt Bder Operation 2immer über die Änderungen informiert ist, die der Thread Аwährend der Operation vorgenommen hat 1. Schauen wir sie uns an.

Regel 1.

Das Freigeben eines Mutex erfolgt, bevor ein anderer Thread denselben Monitor erhält. Nun, hier scheint alles klar zu sein. Wenn der Mutex eines Objekts oder einer Klasse von einem Thread, beispielsweise einem Thread, erfasst wird, kann ihn Аein anderer Thread (Thread B) nicht gleichzeitig erwerben. Sie müssen warten, bis der Mutex freigegeben wird.

Regel 2.

Thread.start() Das passiert vor method Thread.run(). Auch nichts Kompliziertes. Sie wissen bereits: Damit der Code in der Methode ausgeführt werden kann run(), müssen Sie die Methode im Thread aufrufen start(). Es ist seine und nicht die Methode selbst run()! Diese Regel stellt sicher, dass Thread.start()die Werte aller vor der Ausführung festgelegten Variablen in der Methode sichtbar sind, die mit der Ausführung begonnen hat run().

Regel 3.

Der Abschluss der Methode run() erfolgt vor dem Beenden der Methode join(). Kehren wir zu unseren beiden Streams zurück – Аund B. Wir rufen die Methode join()so auf, dass der Thread Bbis zum Abschluss warten muss A, bevor er seine Arbeit ausführen kann. Dies bedeutet, dass die Methode run()von Objekt A auf jeden Fall bis zum Ende ausgeführt wird. Und alle Änderungen an den Daten, die in der run()Thread- Methode auftreten A, werden im Thread vollständig sichtbar sein, Bwenn er auf den Abschluss wartet Aund beginnt, selbst zu arbeiten.

Regel 4.

Das Schreiben in eine flüchtige Variable erfolgt vor dem Lesen derselben Variablen. Durch die Verwendung des Schlüsselworts volatile erhalten wir tatsächlich immer den aktuellen Wert. Auch im Fall von longund double, deren Probleme bereits früher besprochen wurden. Wie Sie bereits wissen, sind in einigen Threads vorgenommene Änderungen für andere Threads nicht immer sichtbar. Aber natürlich gibt es sehr oft Situationen, in denen uns ein solches Programmverhalten nicht passt. Nehmen wir an, wir haben einer Variablen in einem Thread einen Wert zugewiesen A:
int z;.

z= 555;
Wenn unser Thread Bden Wert einer Variablen zauf der Konsole ausgeben würde, könnte er leicht 0 ausgeben, da er den ihm zugewiesenen Wert nicht kennt. Regel 4 garantiert uns also: Wenn Sie eine Variable zals flüchtig deklarieren, sind Änderungen an ihren Werten in einem Thread immer in einem anderen Thread sichtbar. Wenn wir das Wort volatile zum vorherigen Code hinzufügen ...
volatile int z;.

z= 555;
...die Situation, in der der Stream B0 an die Konsole ausgibt, ist ausgeschlossen. Das Schreiben in flüchtige Variablen erfolgt vor dem Lesen aus ihnen.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION