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.