Sie fragen speziell, wie sie intern funktionieren . Hier sind Sie also:
Keine Synchronisation
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Es liest im Grunde genommen Werte aus dem Speicher, erhöht sie und speichert sie wieder im Speicher. Dies funktioniert in einem einzelnen Thread, aber heutzutage funktioniert es im Zeitalter von Multi-Core-, Multi-CPU- und Multi-Level-Caches nicht mehr richtig. Zunächst wird die Race-Bedingung (mehrere Threads können den Wert gleichzeitig lesen), aber auch Sichtbarkeitsprobleme eingeführt. Der Wert wird möglicherweise nur im " lokalen " CPU-Speicher (etwas Cache) gespeichert und ist für andere CPUs / Kerne (und damit - Threads) nicht sichtbar. Aus diesem Grund beziehen sich viele auf die lokale Kopie einer Variablen in einem Thread. Es ist sehr unsicher. Betrachten Sie diesen beliebten, aber fehlerhaften Code zum Stoppen von Threads:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
volatile
Zur stopped
Variablen hinzufügen und es funktioniert einwandfrei - wenn ein anderer Thread geändert wirdstopped
Variable über eine pleaseStop()
Methode , wird diese Änderung garantiert sofort in der while(!stopped)
Schleife des Arbeitsthreads angezeigt . Übrigens ist dies auch keine gute Möglichkeit, einen Thread zu unterbrechen. Weitere Informationen finden Sie unter: So stoppen Sie einen Thread, der für immer ohne Verwendung ausgeführt wird, und Stoppen eines bestimmten Java-Threads .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Die AtomicInteger
Klasse verwendet CAS -CPU-Operationen ( Compare-and-Swap ) auf niedriger Ebene (keine Synchronisierung erforderlich!). Mit ihnen können Sie eine bestimmte Variable nur ändern, wenn der aktuelle Wert gleich etwas anderem ist (und erfolgreich zurückgegeben wird). Wenn Sie es also ausführen, wird getAndIncrement()
es tatsächlich in einer Schleife ausgeführt (vereinfachte reale Implementierung):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Also im Grunde: lesen; Versuchen Sie, einen inkrementierten Wert zu speichern. Wenn dies nicht erfolgreich ist (der Wert ist nicht mehr gleich current
), lesen Sie es und versuchen Sie es erneut. Das compareAndSet()
ist in nativem Code (Assembly) implementiert.
volatile
ohne Synchronisation
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Dieser Code ist nicht korrekt. Es behebt das Sichtbarkeitsproblem ( volatile
stellt sicher, dass andere Threads Änderungen sehen können, an denen Änderungen vorgenommen wurden counter
), hat jedoch weiterhin eine Race-Bedingung. Dies wurde mehrfach erklärt : Pre / Post-Inkrementierung ist nicht atomar.
Die einzige Nebenwirkung von volatile
ist das " Leeren " von Caches, sodass alle anderen Parteien die aktuellste Version der Daten sehen. Dies ist in den meisten Situationen zu streng. Deshalb volatile
ist nicht Standard.
volatile
ohne Synchronisation (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
Das gleiche Problem wie oben, aber noch schlimmer, weil i
es nicht ist private
. Die Rennbedingung ist noch vorhanden. Warum ist das ein Problem? Wenn beispielsweise zwei Threads diesen Code gleichzeitig ausführen, lautet die Ausgabe möglicherweise + 5
oder + 10
. Sie werden jedoch garantiert die Änderung sehen.
Mehrfach unabhängig synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Überraschung, dieser Code ist auch falsch. In der Tat ist es völlig falsch. Zunächst synchronisieren Sie auf i
, was geändert werden soll (außerdem i
ist es ein Grundelement, also denke ich, dass Sie auf einer temporären Seite synchronisierenInteger
das über Autoboxing erstellt wurde ...). Vollständig fehlerhaft. Sie könnten auch schreiben:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
Keine zwei Threads können denselben synchronized
Block betreten mit derselben Sperre . In diesem Fall (und ähnlich in Ihrem Code) ändert sich das Sperrobjekt bei jeder Ausführungsynchronized
effektiv keine Auswirkung.
Selbst wenn Sie eine endgültige Variable (oder this
) für die Synchronisation verwendet haben, ist der Code immer noch falsch. Zwei Fäden können zuerst gelesen , i
um temp
synchron (mit dem gleichen Wert in lokaltemp
), dann werden die ersten ordnet einen neuen Wert i
(sagen wir von 1 bis 6) und die andere tut dasselbe (1 bis 6).
Die Synchronisation muss vom Lesen bis zum Zuweisen eines Wertes reichen. Ihre erste Synchronisation hat keine Auswirkung (das Lesen von int
ist atomar) und die zweite ebenfalls. Meiner Meinung nach sind dies die richtigen Formen:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}