Eine sensible Operation in meinem Labor ist heute völlig schief gelaufen. Ein Aktuator auf einem Elektronenmikroskop überschritt seine Grenze, und nach einer Reihe von Ereignissen verlor ich 12 Millionen Dollar an Ausrüstung. Ich habe über 40K Zeilen im fehlerhaften Modul auf dieses 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 für die Ausgabe, die ich erhalte:
$ 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 bei Überlauf in Java gut verhalten, würde ich denken, dass an diesem Code nichts falsch ist. Obwohl die Ausgabe angibt, dass das Programm die Beendigungsbedingung nicht erreicht hat, hat es die Beendigungsbedingung erreicht (es wurden beide erreicht und nicht erreicht?) . Warum?
Ich habe festgestellt, dass dies in einigen Umgebungen nicht der Fall ist. Ich bin auf OpenJDK 6 unter 64-Bit-Linux.
Offensichtlich geschieht das Schreiben an currentPos nicht - bevor es gelesen wird, aber ich sehe nicht, wie das das Problem sein kann.
currentPos = new Point(currentPos.x+1, currentPos.y+1);
führt einige Dinge aus, einschließlich des Schreibens von Standardwerten in x
und y
(0) und des anschließenden Schreibens ihrer Anfangswerte in den Konstruktor. Da Ihr Objekt nicht sicher veröffentlicht wurde, können diese 4 Schreibvorgänge vom Compiler/JVM frei nachbestellt werden.
Aus Sicht des Lese-Threads ist es also eine legale Ausführung, x
mit seinem neuen Wert zu lesen, aber y
beispielsweise mit seinem Standardwert von 0. Wenn Sie die Anweisung println
erreichen (die übrigens synchronisiert ist und daher die Leseoperationen beeinflusst), haben die Variablen ihre Anfangswerte und das Programm gibt die erwarteten Werte aus.
Wenn Sie currentPos
als volatile
markieren, wird eine sichere Veröffentlichung gewährleistet, da Ihr Objekt tatsächlich unveränderlich ist. Wenn in Ihrem tatsächlichen Anwendungsfall das Objekt nach der Erstellung mutiert wird, reichen die Garantien für volatile
nicht aus und Sie konnten wieder ein inkonsistentes Objekt sehen.
Alternativ können Sie das Point
unveränderlich machen, was auch ohne die Verwendung von volatile
eine sichere Veröffentlichung gewährleistet. Um Unveränderlichkeit zu erreichen, müssen Sie nur x
und y
final markieren.
Als Randnotiz und wie bereits erwähnt, kann synchronized(this) {}
von der JVM als No-Op behandelt werden (ich verstehe, dass Sie es einbezogen haben, um das Verhalten zu reproduzieren).
Da currentPos
außerhalb des Threads geändert wird, sollte es als volatile
markiert werden:
static volatile Point currentPos = new Point(1,2);
Ohne Flüchtigkeit 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 zuvor 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 im Thread lesen, um sie zu vergleichen und anschließend anzuzeigen. Dabei wird x
immer als 1
Und y
zwischen 0
Und einer großen Ganzzahl angezeigt. Ich denke, das Verhalten ist an dieser Stelle etwas undefiniert ohne das Schlüsselwort volatile
und es ist möglich, dass die JIT-Kompilierung des Codes so dazu beiträgt. Auch wenn ich den leeren synchronized(this) {}
-Block auskommentiere, funktioniert der Code ebenfalls und ich vermute, dass das Sperren eine ausreichende Verzögerung verursacht, sodass currentPos
und seine Felder erneut gelesen werden, anstatt aus dem Cache verwendet zu werden .
int x = p.x + 1;
int y = p.y;
if (x != y) {
System.out.println(x+" "+y);
System.exit(1);
}
Sie haben gewöhnlichen Speicher, die 'currentpos'-Referenz und das Point-Objekt und seine Felder dahinter, die von 2 Threads ohne Synchronisation gemeinsam genutzt werden. Daher gibt es keine definierte Reihenfolge zwischen den Schreibvorgängen, die in diesem Speicher im Hauptthread stattfinden, und den Lesevorgängen im erstellten Thread (nennen Sie es T).
Der Haupt-Thread führt die folgenden Schreibvorgänge aus (das Ignorieren der anfänglichen Einrichtung von point führt dazu, dass p.x und p.y Standardwerte haben):
Da diese Schreibvorgänge nichts Besonderes in Bezug auf Synchronisation/Barrieren sind, ist die Laufzeit frei, damit der T-Thread sie in beliebiger Reihenfolge sehen kann (der Hauptthread sieht natürlich immer Schreibvorgänge und Lesevorgänge, die gemäß der Programmreihenfolge angeordnet sind) und auftreten kann an jedem Punkt zwischen den Lesungen 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 dies zu Ihrem Ergebnis führen kann, da T möglicherweise den Schreibvorgang von main in currentpos before die Schreibvorgänge in currentpos sieht .y oder currentpos.x:
und so weiter ... Hier gibt es eine Reihe von Datenrennen.
Ich vermute, dass die fehlerhafte Annahme hier lautet, dass die aus dieser Zeile resultierenden Schreibvorgänge in allen Threads in der Programmreihenfolge des Threads sichtbar gemacht werden, der sie ausführt:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Java gibt keine solche Garantie (es wäre für die Leistung schrecklich). Es muss etwas mehr hinzugefügt werden, wenn Ihr Programm eine garantierte Reihenfolge der Schreibvorgänge im Verhältnis zu den Lesevorgängen in anderen Threads benötigt. Andere haben vorgeschlagen, die x-, y-Felder endgültig oder alternativ flüchtig zu machen.
Die Verwendung von final hat den Vorteil, dass die Felder unveränderlich werden und somit die Werte zwischengespeichert werden können. Die Verwendung von Volatile führt zu einer Synchronisation bei jedem Schreiben und Lesen von CurrentPos, 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
(Die ursprüngliche Antwort ging von einem schwächeren Speichermodell aus, da ich nicht sicher war, ob die JLS-garantierte Flüchtigkeit ausreicht. Die Antwort wurde bearbeitet, um den Kommentar von Assylias widerzuspiegeln. Der Hinweis auf das Java -Modell ist stärker - passiert, bevor es transitiv ist - und daher flüchtig auf currentpos genügt auch).