Eine sensible Operation in meinem Labor ist heute völlig schief gelaufen. Ein Aktuator auf einem Elektronenmikroskop überschritt seine Grenzen, und nach einer Kette von Ereignissen verlor ich 12 Millionen Dollar an Ausrüstung. Ich habe über 40K Zeilen im fehlerhaften Modul auf diese eingegrenzt:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Einige Beispiele der Ausgabe, die ich bekomme:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Da es hier keine Gleitkomma-Arithmetik gibt und wir alle wissen, dass sich vorzeichenbehaftete Ganzzahlen beim Überlauf in Java gut verhalten, würde ich denken, dass an diesem Code nichts falsch ist. Obwohl die Ausgabe anzeigt, dass das Programm die Beendigungsbedingung nicht erreicht hat, hat es die Beendigungsbedingung erreicht (es wurde sowohl erreicht als auch nicht erreicht?). Warum?
Ich habe festgestellt, dass dies in einigen Umgebungen nicht der Fall ist. Ich bin auf OpenJDK 6 unter 64-Bit-Linux.
final
Qualifikators (der keinen Einfluss auf den erzeugten Bytecode hat) zu den Feldernx
undy
"Beheben" des Fehlers. Obwohl dies keinen Einfluss auf den Bytecode hat, sind die Felder damit gekennzeichnet, was mich zu der Annahme veranlasst, dass dies ein Nebeneffekt einer JVM-Optimierung ist.Point
p
wird konstruiert, was erfülltp.x+1 == p.y
, dann wird eine Referenz an den Abfragethread übergeben. Schließlich entscheidet sich der Abfragethread zum Beenden, weil er der Meinung ist, dass die Bedingung für eines derPoint
empfangenen s nicht erfüllt ist , aber dann zeigt die Konsolenausgabe, dass sie erfüllt sein sollte. Das Fehlen vonvolatile
hier bedeutet einfach, dass der Polling-Thread stecken bleiben kann, aber das ist hier eindeutig nicht das Problem.synchronized
dass der Fehler nicht auftritt? Das liegt daran, dass ich zufällig Code schreiben musste, bis ich einen fand, der dieses Verhalten deterministisch reproduziert.Antworten:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
führt einige Dinge aus, einschließlich des Schreibens von Standardwerten inx
undy
(0) und des anschließenden Schreibens ihrer Anfangswerte in den Konstruktor. Da Ihr Objekt nicht sicher veröffentlicht wird, können diese 4 Schreibvorgänge vom Compiler / JVM frei neu angeordnet werden.Aus der Sicht des Lesethreads ist es also eine legale Ausführung,
x
mit seinem neuen Wert zu lesen , aber beispielsweisey
mit seinem Standardwert 0. Wenn Sie dieprintln
Anweisung erreichen (die übrigens synchronisiert ist und daher die Leseoperationen beeinflusst), haben die Variablen ihre Anfangswerte und das Programm druckt die erwarteten Werte.Das Markieren
currentPos
alsvolatile
gewährleistet eine sichere Veröffentlichung, da Ihr Objekt effektiv unveränderlich ist. Wenn das Objekt in Ihrem tatsächlichen Anwendungsfall nach der Erstellung mutiertvolatile
wird, reichen Garantien nicht aus und Sie können ein inkonsistentes Objekt erneut sehen.Alternativ können Sie das
Point
Unveränderliche herstellen, wodurch auch ohne Verwendung eine sichere Veröffentlichung gewährleistet wirdvolatile
. Um Unveränderlichkeit zu erreichen, müssen Sie nur markierenx
undy
abschließen.Als Randnotiz und wie bereits erwähnt,
synchronized(this) {}
kann von der JVM als No-Op behandelt werden (ich verstehe, dass Sie es aufgenommen haben, um das Verhalten zu reproduzieren).quelle
Da
currentPos
außerhalb des Threads geändert wird, sollte es markiert werden alsvolatile
:Ohne flüchtig kann der Thread nicht garantiert Aktualisierungen von currentPos einlesen, die im Hauptthread vorgenommen werden. Daher werden weiterhin neue Werte für currentPos geschrieben, aber der Thread verwendet aus Leistungsgründen weiterhin die vorherigen zwischengespeicherten Versionen. Da nur ein Thread currentPos ändert, können Sie ohne Sperren davonkommen, was die Leistung verbessert.
Die Ergebnisse sehen sehr unterschiedlich aus, wenn Sie die Werte nur ein einziges Mal innerhalb des Threads lesen, um sie zu vergleichen und anschließend anzuzeigen. Wenn ich dies tue, wird Folgendes
x
immer als1
undy
zwischen0
einer großen Ganzzahl angezeigt . Ich denke, das Verhalten an dieser Stelle ist ohne dasvolatile
Schlüsselwort etwas undefiniert und es ist möglich, dass die JIT-Kompilierung des Codes dazu beiträgt, dass es sich so verhält. Auch wenn ich den leerensynchronized(this) {}
Block auskommentiere, funktioniert der Code ebenfalls und ich vermute, dass das Sperren eine ausreichende Verzögerung verursachtcurrentPos
und die Felder erneut gelesen und nicht aus dem Cache verwendet werden.quelle
volatile
.Sie haben normalen Speicher, die 'currentpos'-Referenz und das Point-Objekt und seine Felder dahinter, die von zwei Threads ohne Synchronisation gemeinsam genutzt werden. Daher gibt es keine definierte Reihenfolge zwischen den Schreibvorgängen, die in diesem Speicher im Hauptthread ausgeführt werden, und den Lesevorgängen im erstellten Thread (nennen Sie es T).
Der Hauptthread führt die folgenden Schreibvorgänge aus (das Ignorieren der anfänglichen Einrichtung von point führt dazu, dass px und py Standardwerte haben):
Da diese Schreibvorgänge in Bezug auf Synchronisation / Barrieren nichts Besonderes sind, ist die Laufzeit frei, damit der T-Thread sie in beliebiger Reihenfolge sehen kann (der Haupt-Thread sieht natürlich immer Schreib- und Lesevorgänge, die gemäß der Programmreihenfolge sortiert sind) und auftreten zu jedem Zeitpunkt zwischen den Lesevorgängen in T.
Also macht T:
Da es keine Ordnungsbeziehungen zwischen den Schreibvorgängen in main und den Lesevorgängen in T gibt, gibt es eindeutig mehrere Möglichkeiten, wie Sie Ihr Ergebnis erzielen können, da T möglicherweise das Schreiben von main in currentpos vor den Schreibvorgängen in currentpos.y oder currentpos.x sieht:
und so weiter ... Hier gibt es eine Reihe von Datenrennen.
Ich vermute, dass die fehlerhafte Annahme hier darin besteht, dass die aus dieser Zeile resultierenden Schreibvorgänge über alle Threads in der Programmreihenfolge des Threads sichtbar gemacht werden, der sie ausführt:
Java gibt keine solche Garantie (es wäre schrecklich für die Leistung). Es muss noch etwas hinzugefügt werden, wenn Ihr Programm eine garantierte Reihenfolge der Schreibvorgänge in Bezug auf Lesevorgänge in anderen Threads benötigt. Andere haben vorgeschlagen, die x-, y-Felder endgültig zu machen oder alternativ currentpos flüchtig zu machen.
Die Verwendung von final hat den Vorteil, dass die Felder unveränderlich werden und die Werte zwischengespeichert werden können. Die Verwendung von flüchtigen Daten führt zu einer Synchronisierung bei jedem Schreiben und Lesen von aktuellen Poss, was die Leistung beeinträchtigen kann.
Weitere Informationen finden Sie in Kapitel 17 der Java-Sprachspezifikation: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(Bei der ersten Antwort wurde von einem schwächeren Speichermodell ausgegangen, da ich nicht sicher war, ob die von JLS garantierte Volatilität ausreicht. Die Antwort wurde bearbeitet, um den Kommentar von Assylias widerzuspiegeln ).
quelle
currentPos
gewährleistet die Zuweisung , wenn sie flüchtig gemacht wird, eine sichere Veröffentlichung descurrentPos
Objekts sowie seiner Mitglieder, auch wenn sie selbst nicht flüchtig sind.Point currentPos = new Point(x, y)
Sie haben 3 Schreibvorgänge: (w1)this.x = x
, (w2)this.y = y
und (w3)currentPos = the new point
. Die Programmreihenfolge garantiert, dass hb (w1, w3) und hb (w2, w3). Später im Programm lesen Sie (r1)currentPos
. WenncurrentPos
es nicht flüchtig ist, gibt es kein hb zwischen r1 und w1, w2, w3, so dass r1 irgendeinen (oder keinen) von ihnen beobachten kann. Mit volatile führen Sie hb (w3, r1) ein. Und die hb-Beziehung ist transitiv, so dass Sie auch hb (w1, r1) und hb (w2, r1) einführen. Dies ist in Java Concurrency in Practice (3.5.3. Safe Publication Idioms) zusammengefasst.Sie können ein Objekt verwenden, um die Schreib- und Lesevorgänge zu synchronisieren. Andernfalls erfolgt, wie bereits erwähnt, ein Schreiben in currentPos in der Mitte der beiden Lesevorgänge p.x + 1 und py
quelle
sem
nicht geteilt wird, und die synchronisierte Anweisung als No-Op behandeln ... Die Tatsache, dass sie das Problem löst, ist reines Glück.Sie greifen zweimal auf currentPos zu und geben keine Garantie dafür, dass es zwischen diesen beiden Zugriffen nicht aktualisiert wird.
Beispielsweise:
Sie vergleichen im Wesentlichen zwei verschiedene Punkte.
Beachten Sie, dass selbst wenn Sie currentPos flüchtig machen, Sie nicht davor geschützt sind, da es zwei separate Lesevorgänge durch den Worker-Thread sind.
Fügen Sie eine hinzu
Methode zu Ihrer Punkteklasse. Dadurch wird sichergestellt, dass bei der Überprüfung von x + 1 == y nur ein Wert von currentPos verwendet wird.
quelle