Es ist wichtig zu verstehen, dass die Thread-Sicherheit zwei Aspekte hat.
- Ausführungskontrolle und
- Speichersichtbarkeit
Das erste hat damit zu tun, zu steuern, wann Code ausgeführt wird (einschließlich der Reihenfolge, in der Anweisungen ausgeführt werden) und ob er gleichzeitig ausgeführt werden kann, und das zweite damit, wann die Auswirkungen im Speicher dessen, was getan wurde, für andere Threads sichtbar sind. Da jede CPU mehrere Cache-Ebenen zwischen sich und dem Hauptspeicher hat, können Threads, die auf verschiedenen CPUs oder Kernen ausgeführt werden, "Speicher" zu einem bestimmten Zeitpunkt unterschiedlich sehen, da Threads private Kopien des Hauptspeichers abrufen und bearbeiten dürfen.
Durch synchronized
die Verwendung wird verhindert, dass ein anderer Thread den Monitor (oder die Sperre) für dasselbe Objekt erhält , wodurch verhindert wird, dass alle durch die Synchronisation für dasselbe Objekt geschützten Codeblöcke gleichzeitig ausgeführt werden. Durch die Synchronisierung wird auch eine "Barriere vor" -Speicherbarriere erstellt, die eine Einschränkung der Speichersichtbarkeit verursacht, sodass alles, was bis zu dem Punkt getan wird, an dem ein Thread eine Sperre aufhebt , einem anderen Thread angezeigt wird , der anschließend dieselbe Sperre erhält , die vor dem Erwerb der Sperre aufgetreten ist. In der Praxis führt dies bei aktueller Hardware normalerweise zum Leeren der CPU-Caches, wenn ein Monitor erfasst wird, und zum Schreiben in den Hauptspeicher, wenn er freigegeben wird. Beide sind (relativ) teuer.
Die Verwendung volatile
erzwingt andererseits, dass alle Zugriffe (Lesen oder Schreiben) auf die flüchtige Variable auf den Hauptspeicher erfolgen, wodurch die flüchtige Variable effektiv aus den CPU-Caches herausgehalten wird. Dies kann für einige Aktionen nützlich sein, bei denen lediglich die Sichtbarkeit der Variablen korrekt sein muss und die Reihenfolge der Zugriffe nicht wichtig ist. Die Verwendung volatile
ändert auch die Behandlung von long
und double
erfordert, dass Zugriffe auf sie atomar sind; Bei einigen (älteren) Hardwarekomponenten sind möglicherweise Sperren erforderlich, bei moderner 64-Bit-Hardware jedoch nicht. Unter dem neuen Speichermodell (JSR-133) für Java 5+ wurde die Semantik von flüchtigen Verbindungen so stark wie synchronisiert in Bezug auf Speichersichtbarkeit und Befehlsreihenfolge gestärkt (siehe http://www.cs.umd.edu) /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Aus Gründen der Sichtbarkeit wirkt jeder Zugriff auf ein flüchtiges Feld wie eine halbe Synchronisation.
Unter dem neuen Speichermodell ist es immer noch richtig, dass flüchtige Variablen nicht miteinander neu angeordnet werden können. Der Unterschied besteht darin, dass es jetzt nicht mehr so einfach ist, normale Feldzugriffe um sie herum neu zu ordnen. Das Schreiben in ein flüchtiges Feld hat den gleichen Speichereffekt wie eine Monitorfreigabe, und das Lesen aus einem flüchtigen Feld hat den gleichen Speichereffekt wie ein Monitorerwerb. Da das neue Speichermodell die Neuordnung flüchtiger Feldzugriffe mit anderen Feldzugriffen, ob flüchtig oder nicht, strenger einschränkt, wird alles, was A
beim Schreiben in ein flüchtiges Feld f
für den Thread sichtbar war, B
beim Lesen für den Thread sichtbar f
.
- Häufig gestellte Fragen zu JSR 133 (Java Memory Model)
Daher verursachen beide Formen der Speicherbarriere (unter dem aktuellen JMM) eine Barriere für die Neuordnung von Befehlen, die verhindert, dass der Compiler oder die Laufzeit Befehle über die Barriere hinweg neu anordnen. Im alten JMM verhinderte flüchtig nicht die Nachbestellung. Dies kann wichtig sein, da abgesehen von Speicherbarrieren die einzige Einschränkung darin besteht, dass für einen bestimmten Thread der Nettoeffekt des Codes der gleiche ist, als wenn die Anweisungen genau in der Reihenfolge ausgeführt würden, in der sie in der erscheinen Quelle.
Eine Verwendung von flüchtig ist, dass ein gemeinsam genutztes, aber unveränderliches Objekt im laufenden Betrieb neu erstellt wird, wobei viele andere Threads zu einem bestimmten Zeitpunkt in ihrem Ausführungszyklus auf das Objekt verweisen. Die anderen Threads müssen das neu erstellte Objekt verwenden, sobald es veröffentlicht ist, benötigen jedoch nicht den zusätzlichen Aufwand für die vollständige Synchronisierung und die damit verbundenen Konflikte und das Leeren des Caches.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Sprechen Sie speziell mit Ihrer Frage zum Lesen, Aktualisieren und Schreiben. Betrachten Sie den folgenden unsicheren Code:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Wenn die updateCounter () -Methode nicht synchronisiert ist, können zwei Threads gleichzeitig darauf zugreifen. Unter den vielen Permutationen dessen, was passieren könnte, ist eine, dass Thread-1 den Test für den Zähler == 1000 durchführt und ihn für wahr hält und dann ausgesetzt wird. Dann führt Thread-2 den gleichen Test durch und sieht ihn auch als wahr an und wird angehalten. Dann wird Thread-1 fortgesetzt und der Zähler auf 0 gesetzt. Dann wird Thread-2 fortgesetzt und setzt den Zähler erneut auf 0, da das Update von Thread-1 verpasst wurde. Dies kann auch passieren, wenn der Thread-Wechsel nicht wie beschrieben erfolgt, sondern einfach, weil zwei verschiedene zwischengespeicherte Kopien des Zählers in zwei verschiedenen CPU-Kernen vorhanden waren und die Threads jeweils auf einem separaten Kern liefen. In diesem Fall könnte ein Thread einen Zähler bei einem Wert und der andere einen Zähler bei einem völlig anderen Wert haben, nur weil er zwischengespeichert ist.
Was in diesem Beispiel wichtig ist , dass die Variable Zähler wurde aus dem Hauptspeicher in den Cache gelesen, im Cache aktualisiert und erst später in den Hauptspeicher an einem gewissen unbestimmten Punkt zurückgeschrieben , wenn eine Speicherbarriere aufgetreten ist oder wenn der Cache - Speicher wurde für etwas anderes benötigt. Das Erstellen des Zählers reicht volatile
für die Thread-Sicherheit dieses Codes nicht aus, da der Test für das Maximum und die Zuweisungen diskrete Operationen sind, einschließlich des Inkrements, bei dem es sich um einen Satz nichtatomarer read+increment+write
Maschinenanweisungen handelt, wie z.
MOV EAX,counter
INC EAX
MOV counter,EAX
Flüchtige Variablen sind nur dann nützlich, wenn alle an ihnen ausgeführten Operationen "atomar" sind, wie in meinem Beispiel, in dem ein Verweis auf ein vollständig geformtes Objekt nur gelesen oder geschrieben wird (und in der Tat normalerweise nur von einem einzelnen Punkt aus geschrieben wird). Ein anderes Beispiel wäre eine flüchtige Array-Referenz, die eine Copy-on-Write-Liste unterstützt, vorausgesetzt, das Array wurde nur gelesen, indem zuerst eine lokale Kopie der Referenz darauf erstellt wurde.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
quelle
synchronized
ist der Zugriffsbeschränkungsmodifikator auf Methoden- / Blockebene. Dadurch wird sichergestellt, dass ein Thread die Sperre für kritische Abschnitte besitzt. Nur der Thread, der eine Sperre besitzt, kann in densynchronized
Block eintreten . Wenn andere Threads versuchen, auf diesen kritischen Abschnitt zuzugreifen, müssen sie warten, bis der aktuelle Eigentümer die Sperre aufhebt.volatile
ist ein Modifikator für den variablen Zugriff, der alle Threads dazu zwingt, den neuesten Wert der Variablen aus dem Hauptspeicher abzurufen. Für den Zugriff aufvolatile
Variablen ist keine Sperrung erforderlich . Alle Threads können gleichzeitig auf den Wert der flüchtigen Variablen zugreifen.Ein gutes Beispiel für die Verwendung einer flüchtigen Variablen:
Date
Variable.Angenommen, Sie haben die Datumsvariable festgelegt
volatile
. Alle Threads, die auf diese Variable zugreifen, erhalten immer die neuesten Daten aus dem Hauptspeicher, sodass alle Threads den tatsächlichen (tatsächlichen) Datumswert anzeigen. Sie benötigen keine unterschiedlichen Threads, die unterschiedliche Zeiten für dieselbe Variable anzeigen. Alle Threads sollten den richtigen Datumswert anzeigen.Schauen Sie sich diesen Artikel an, um das
volatile
Konzept besser zu verstehen .Lawrence Dol Cleary erklärte Ihre
read-write-update query
.In Bezug auf Ihre anderen Fragen
Sie müssen verwenden,
volatile
wenn Sie der Meinung sind, dass alle Threads den tatsächlichen Wert der Variablen in Echtzeit erhalten sollen, wie in dem Beispiel, das ich für die Datumsvariable erläutert habe.Die Antwort ist dieselbe wie bei der ersten Abfrage.
Weitere Informationen finden Sie in diesem Artikel .
quelle
tl; dr :
Beim Multithreading gibt es drei Hauptprobleme:
1) Rennbedingungen
2) Caching / veralteter Speicher
3) Complier- und CPU-Optimierungen
volatile
kann 2 & 3 lösen, aber nicht 1.synchronized
/ Explizite Sperren können 1, 2 & 3 lösen.Ausarbeitung :
1) Betrachten Sie diesen Thread als unsicheren Code:
x++;
Während es wie eine Operation aussehen mag, ist es tatsächlich 3: Lesen des aktuellen Werts von x aus dem Speicher, Hinzufügen von 1 und Speichern im Speicher. Wenn nur wenige Threads gleichzeitig versuchen, dies zu tun, ist das Ergebnis der Operation undefiniert. Wenn
x
ursprünglich 1 war, kann es nach 2 Threads, die den Code ausführen, 2 und 3 sein, abhängig davon, welcher Thread welchen Teil der Operation abgeschlossen hat, bevor die Steuerung auf den anderen Thread übertragen wurde. Dies ist eine Form der Rennbedingung .Die Verwendung
synchronized
eines Codeblocks macht ihn atomar - was bedeutet, dass es so aussieht, als würden die drei Operationen gleichzeitig ausgeführt, und es gibt keine Möglichkeit, dass ein anderer Thread in die Mitte kommt und sich einmischt. Wenn alsox
1 und 2 Threads versuchen, sich vorzubereitenx++
, wissen wir, dass es am Ende gleich 3 sein wird. Damit wird das Problem der Rennbedingungen gelöst.Das Markieren
x
alsvolatile
macht nichtx++;
atomar, daher wird dieses Problem nicht gelöst.2) Außerdem haben Threads einen eigenen Kontext, dh sie können Werte aus dem Hauptspeicher zwischenspeichern. Das bedeutet, dass einige Threads Kopien einer Variablen haben können, aber sie arbeiten mit ihrer Arbeitskopie, ohne den neuen Status der Variablen mit anderen Threads zu teilen.
Betrachten Sie das auf einem Thread ,
x = 10;
. Und etwas später in einem anderen Threadx = 20;
. Die Wertänderung von wirdx
möglicherweise nicht im ersten Thread angezeigt, da der andere Thread den neuen Wert in seinem Arbeitsspeicher gespeichert, ihn jedoch nicht in den Hauptspeicher kopiert hat. Oder dass es es in den Hauptspeicher kopiert hat, aber der erste Thread seine Arbeitskopie nicht aktualisiert hat. Wenn nun der erste Thread prüft, lautetif (x == 20)
die Antwortfalse
.Das Markieren einer Variablen als
volatile
weist im Grunde alle Threads an, Lese- und Schreibvorgänge nur im Hauptspeicher auszuführen.synchronized
Weist jeden Thread an, seinen Wert aus dem Hauptspeicher zu aktualisieren, wenn er den Block betritt, und das Ergebnis beim Verlassen des Blocks in den Hauptspeicher zurückzuspülen.Beachten Sie, dass veralteter Speicher im Gegensatz zu Datenrassen nicht so einfach (neu) zu erstellen ist, da ohnehin Leergut in den Hauptspeicher erfolgt.
3) Der Complier und die CPU können (ohne jegliche Form der Synchronisation zwischen Threads) den gesamten Code als Single-Threading behandeln. Das heißt, es kann sich einen Code ansehen, der in einem Multithreading-Aspekt sehr bedeutsam ist, und ihn so behandeln, als wäre er Single-Threaded, wo er nicht so aussagekräftig ist. Es kann sich also einen Code ansehen und aus Gründen der Optimierung entscheiden, ihn neu zu ordnen oder sogar Teile davon vollständig zu entfernen, wenn es nicht weiß, dass dieser Code für die Verwendung mit mehreren Threads ausgelegt ist.
Betrachten Sie den folgenden Code:
Sie würden denken, dass threadB nur 20 drucken könnte (oder überhaupt nichts drucken könnte, wenn threadB if-check ausgeführt wird, bevor
b
auf true gesetzt wird), dab
true erst gesetzt wird, nachdemx
auf 20 gesetzt wurde, aber der Compiler / die CPU könnte sich für eine Neuordnung entscheiden threadA, in diesem Fall könnte threadB auch 10 drucken. Durch Markierenb
als wirdvolatile
sichergestellt, dass es nicht neu angeordnet (oder in bestimmten Fällen verworfen) wird. Was bedeutet, dass threadB nur 20 (oder gar nichts) drucken konnte. Durch Markieren der Methoden als synchronisiert wird das gleiche Ergebnis erzielt. Durch das Markieren einer Variablen als wirdvolatile
nur sichergestellt, dass sie nicht neu angeordnet wird, aber alles davor / danach kann noch neu angeordnet werden, sodass die Synchronisierung in einigen Szenarien besser geeignet ist.Beachten Sie, dass volatile dieses Problem vor Java 5 New Memory Model nicht gelöst hat.
quelle
INC
Operation wird, sind die zugrunde liegenden CPU-Operationen immer noch dreifach und erfordern aus Gründen der Thread-Sicherheit eine Sperre. Guter Punkt. Obwohl dieINC/DEC
Befehle in der Assembly atomar markiert werden können und immer noch eine atomare Operation sind.