it-swarm.com.de

Warum wird dieses Java) Programm trotzdem beendet, obwohl es anscheinend nicht funktionieren sollte (und auch nicht)?

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.

205
Dog

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).

140
assylias

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);
}
29
Ed Plese

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):

  • bis p.x.
  • bis p.y.
  • zu aktuellenPos

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:

  1. liest currentpos bis p
  2. lese p.x und p.y (in beliebiger Reihenfolge)
  3. vergleiche und nimm den Zweig
  4. lesen Sie p.x und p.y (jede Bestellung) und rufen Sie System.out.println auf

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:

  1. Es liest zuerst currentpos.x, bevor der x-Schreibvorgang stattgefunden hat - erhält 0, dann liest es currentpos.y, bevor der y-Schreibvorgang stattgefunden hat - erhält 0. Vergleiche evals mit true. Die Schreibvorgänge werden für T. sichtbar. System.out.println wird aufgerufen.
  2. Er liest zuerst currentpos.x, nachdem das x-Schreiben stattgefunden hat, und liest dann currentpos.y, bevor das y-Schreiben stattgefunden hat - erhält 0. Vergleiche evals mit true. Schreibvorgänge werden für T ... usw. sichtbar.
  3. Er liest zuerst currentpos.y, bevor der y-Schreibvorgang stattgefunden hat (0), und liest dann currentpos.x nach dem x-Schreibvorgang. etc.

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.

  • Wenn Sie die Felder x, y als endgültig festlegen, stellt Java sicher, dass die Schreibvorgänge für ihre Werte in allen Threads angezeigt werden, bevor der Konstruktor zurückkehrt. Da die Zuweisung zu currentpos nach dem Konstruktor erfolgt, wird dem T-Thread garantiert, dass die Schreibvorgänge in der richtigen Reihenfolge angezeigt werden.
  • Wenn Sie currentpos flüchtig machen, garantiert Java, dass dies ein Synchronisationspunkt ist, der für andere Synchronisationspunkte nach der Gesamtordnung geordnet wird. Da in der Regel die Schreibvorgänge in x und y vor dem Schreiben in currentpos erfolgen müssen, müssen beim Lesen von currentpos in einem anderen Thread auch die zuvor aufgetretenen Schreibvorgänge in x, y berücksichtigt werden.

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).

19
paulj