Ich weiß, dass zusammengesetzte Operationen, wie sie i++
nicht threadsicher sind, mehrere Operationen beinhalten.
Aber ist das Überprüfen der Referenz mit sich selbst eine thread-sichere Operation?
a != a //is this thread-safe
Ich habe versucht, dies zu programmieren und mehrere Threads zu verwenden, aber es ist nicht fehlgeschlagen. Ich konnte wohl kein Rennen auf meiner Maschine simulieren.
BEARBEITEN:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
Dies ist das Programm, mit dem ich die Thread-Sicherheit teste.
Seltsames Verhalten
Wenn mein Programm zwischen einigen Iterationen startet, erhalte ich den Ausgabeflag-Wert, was bedeutet, dass die Referenzprüfung !=
für dieselbe Referenz fehlschlägt. ABER nach einigen Iterationen wird die Ausgabe zu einem konstanten Wert, false
und wenn das Programm dann über einen langen Zeitraum ausgeführt wird, wird keine einzige true
Ausgabe generiert .
Wie die Ausgabe nach einigen n (nicht festen) Iterationen andeutet, scheint die Ausgabe ein konstanter Wert zu sein und ändert sich nicht.
Ausgabe:
Für einige Iterationen:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
quelle
1234:true
niemals gegenseitig zerschlagen ). Ein Renntest benötigt eine engere innere Schleife. Drucken Sie am Ende eine Zusammenfassung aus (wie unten bei einem Unit-Test-Framework).Antworten:
In Ermangelung einer Synchronisation dieser Code
kann produzieren
true
. Dies ist der Bytecode fürtest()
Wie wir sehen können, wird das Feld
a
zweimal in lokale Variablen geladen. Es handelt sich um eine nichtatomare Operation, wenn siea
zwischendurch durch einen anderen Thread-Vergleich geändert wurdefalse
.Auch das Problem der Speichersichtbarkeit ist hier relevant. Es gibt keine Garantie dafür, dass Änderungen,
a
die von einem anderen Thread vorgenommen wurden, für den aktuellen Thread sichtbar sind.quelle
!=
, bei der LHS und RHS getrennt geladen werden. Wenn das JLS also nichts Spezifisches über Optimierungen erwähnt, wenn LHS und RHS syntaktisch identisch sind, gilt die allgemeine Regel, dha
zweimaliges Laden .Wenn
a
möglicherweise von einem anderen Thread aktualisiert werden kann (ohne ordnungsgemäße Synchronisierung!), Dann Nein.Das hat nichts zu bedeuten! Das Problem ist, dass der Code nicht threadsicher ist, wenn eine Ausführung, die
a
von einem anderen Thread aktualisiert wird , vom JLS zugelassen wird. Die Tatsache, dass Sie nicht veranlassen können, dass die Race-Bedingung mit einem bestimmten Testfall auf einem bestimmten Computer und einer bestimmten Java-Implementierung auftritt, schließt dies unter anderen Umständen nicht aus.Ja, theoretisch unter bestimmten Umständen.
Alternativ
a != a
könnte zurückkehrenfalse
, obwohla
sich gleichzeitig ändert.In Bezug auf das "seltsame Verhalten":
Dieses "seltsame" Verhalten stimmt mit dem folgenden Ausführungsszenario überein:
Das Programm wird geladen und die JVM beginnt mit der Interpretation der Bytecodes. Da (wie wir aus der Javap-Ausgabe gesehen haben) der Bytecode zwei Ladevorgänge ausführt, sehen Sie (anscheinend) gelegentlich die Ergebnisse der Rennbedingung.
Nach einiger Zeit wird der Code vom JIT-Compiler kompiliert. Der JIT-Optimierer stellt fest, dass zwei Ladungen desselben Speichersteckplatzes (
a
) nahe beieinander liegen, und optimiert den zweiten entfernt. (Tatsächlich besteht die Möglichkeit, dass der Test vollständig optimiert wird ...)Jetzt manifestiert sich der Rennzustand nicht mehr, weil es keine zwei Lasten mehr gibt.
Beachten Sie, dass dies alles mit dem übereinstimmt, was das JLS einer Implementierung von Java ermöglicht.
@kriss kommentierte also:
Das Java-Speichermodell (in JLS 17.4 angegeben) gibt eine Reihe von Voraussetzungen an, unter denen ein Thread garantiert Speicherwerte sieht, die von einem anderen Thread geschrieben wurden. Wenn ein Thread versucht, eine von einem anderen geschriebene Variable zu lesen, und diese Voraussetzungen nicht erfüllt sind, kann es eine Reihe möglicher Ausführungen geben, von denen einige wahrscheinlich falsch sind (aus Sicht der Anforderungen der Anwendung). Mit anderen Worten, die Menge möglicher Verhaltensweisen (dh die Menge "wohlgeformter Ausführungen") ist definiert, aber wir können nicht sagen, welches dieser Verhaltensweisen auftreten wird.
Der Compiler kann Lasten kombinieren und neu anordnen und speichern (und andere Dinge tun), vorausgesetzt, der Endeffekt des Codes ist der gleiche:
Wenn der Code jedoch nicht richtig synchronisiert wird (und daher die "Vorher" -Beziehungen die Menge der wohlgeformten Ausführungen nicht ausreichend einschränken), kann der Compiler Lasten und Speichern auf eine Weise neu anordnen, die "falsche" Ergebnisse liefert. (Aber das heißt wirklich nur, dass das Programm falsch ist.)
quelle
a != a
dies wahr sein könnte?Bewiesen mit test-ng:
Ich habe 2 Fehler bei 10 000 Aufrufen. Also NEIN , es ist NICHT threadsicher
quelle
Random.nextInt()
Teil ist überflüssig. Sie hättennew Object()
genauso gut testen können .Nein ist es nicht. Für einen Vergleich muss die Java-VM die beiden zu vergleichenden Werte auf den Stapel legen und die Vergleichsanweisung ausführen (die vom Typ "a" abhängt).
Die Java-VM kann:
false
Im ersten Fall könnte ein anderer Thread den Wert für "a" zwischen den beiden Lesevorgängen ändern.
Welche Strategie gewählt wird, hängt vom Java-Compiler und der Java-Laufzeit ab (insbesondere vom JIT-Compiler). Es kann sich sogar zur Laufzeit Ihres Programms ändern.
Wenn Sie sicherstellen möchten, wie auf die Variable zugegriffen wird, müssen Sie sie erstellen
volatile
(eine sogenannte "halbe Speicherbarriere") oder eine vollständige Speicherbarriere hinzufügen (synchronized
). Sie können auch eine API auf höherer Ebene verwenden (z. B.AtomicInteger
wie von Juned Ahasan erwähnt).Weitere Informationen zur Thread-Sicherheit finden Sie in JSR 133 ( Java Memory Model ).
quelle
a
alsvolatile
würde immer noch zwei unterschiedliche Lesevorgänge implizieren, mit der Möglichkeit eines Wechsels dazwischen.Stephen C. hat alles gut erklärt. Zum Spaß können Sie versuchen, denselben Code mit den folgenden JVM-Parametern auszuführen:
Dies sollte die Optimierung durch die JIT verhindern (dies geschieht auf dem Hotspot 7-Server) und Sie werden es
true
für immer sehen (ich habe bei 2.000.000 angehalten, aber ich nehme an, dass es danach fortgesetzt wird).Zur Information finden Sie unten den JIT-Code. Um ehrlich zu sein, lese ich die Montage nicht fließend genug, um zu wissen, ob der Test tatsächlich durchgeführt wird oder woher die beiden Lasten stammen. (Zeile 26 ist der Test
flag = a != a
und Zeile 31 ist die schließende Klammer vonwhile(true)
).quelle
0x27dccd1
bis0x27dccdf
. Dasjmp
in der Schleife ist bedingungslos (da die Schleife unendlich ist). Die einzigen anderen zwei Anweisungen in der Schleife sindadd rbc, 0x1
- was inkrementell istcountOfIterations
(trotz der Tatsache, dass die Schleife niemals verlassen wird, so dass dieser Wert nicht gelesen wird: Vielleicht wird er benötigt, falls Sie im Debugger in ihn einbrechen), .. .test
Anweisung, die eigentlich nur für den Speicherzugriff vorhanden ist (Hinweis, dereax
in der Methode niemals festgelegt wird!): Es handelt sich um eine spezielle Seite, die auf nicht lesbar gesetzt ist, wenn die JVM alle Threads auslösen möchte Um einen Sicherheitspunkt zu erreichen, kann gc oder eine andere Operation ausgeführt werden, bei der alle Threads in einem bekannten Zustand sein müssen.instance. a != instance.a
Vergleich vollständig aus der Schleife gehoben und führt ihn nur einmal durch, bevor die Schleife betreten wird! Es ist bekannt, dass es nicht erforderlich ist, neu zu laden,instance
odera
da sie nicht als flüchtig deklariert sind und es keinen anderen Code gibt, der sie im selben Thread ändern kann. Daher wird lediglich davon ausgegangen, dass sie während der gesamten Schleife, die vom Speicher zugelassen wird, gleich sind Modell.Nein,
a != a
ist nicht threadsicher. Dieser Ausdruck besteht aus drei Teilen: Ladena
,a
erneut laden und ausführen!=
. Es ist möglich, dass ein anderer Thread die intrinsische Sperre füra
das übergeordnete Element erhält und den Werta
zwischen den beiden Ladevorgängen ändert .Ein weiterer Faktor ist jedoch, ob
a
es sich um eine lokale handelt. Wenna
es lokal ist, sollten keine anderen Threads Zugriff darauf haben und daher threadsicher sein.sollte auch immer drucken
false
.Deklarieren
a
alsvolatile
würde das Problem für ifa
isstatic
oder instance nicht lösen . Das Problem ist nicht, dass Threads unterschiedliche Werte von habena
, sondern dass ein Threada
zweimal mit unterschiedlichen Werten geladen wird. Dies kann dazu führen, dass der Fall weniger threadsicher ist. Wenn diesa
nicht der Fall ist, wirdvolatile
era
möglicherweise zwischengespeichert, und eine Änderung in einem anderen Thread wirkt sich nicht auf den zwischengespeicherten Wert aus.quelle
synchronized
falsch ist : für das Code drucken garantiert werdenfalse
, sind alle Methoden , die Seta
sein müsstensynchronized
, auch.a
das übergeordnete Element erhalten, während die Methode ausgeführt wird, die zum Festlegen des Werts erforderlich ista
.In Bezug auf das seltsame Verhalten:
Da die Variable
a
nicht als markiert istvolatile
, wird der Wert vona
möglicherweise irgendwann vom Thread zwischengespeichert. Beidea
sa != a
sind dann die zwischengespeicherte Version und somit immer gleich (Bedeutungflag
ist jetzt immerfalse
).quelle
Selbst einfaches Lesen ist nicht atomar. Wenn
a
istlong
und nicht alsvolatile
dann markiert, ist bei 32-Bit-JVMslong b = a
nicht threadsicher.quelle