JIT-Optimierungen mit Reflexion brechen

9

Beim Herumspielen mit Komponententests für eine hochkonkurrierende Singleton-Klasse bin ich auf das folgende seltsame Verhalten gestoßen (getestet mit JDK 1.8.0_162):

private static class SingletonClass {
    static final SingletonClass INSTANCE = new SingletonClass(0);
    final int value;

    static SingletonClass getInstance() {
        return INSTANCE;
    }

    SingletonClass(int value) {
        this.value = value;
    }
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

    System.out.println(SingletonClass.getInstance().value); // 0

    // Change the instance to a new one with value 1
    setSingletonInstance(new SingletonClass(1));
    System.out.println(SingletonClass.getInstance().value); // 1

    // Call getInstance() enough times to trigger JIT optimizations
    for(int i=0;i<100_000;++i){
        SingletonClass.getInstance();
    }

    System.out.println(SingletonClass.getInstance().value); // 1

    setSingletonInstance(new SingletonClass(2));
    System.out.println(SingletonClass.INSTANCE.value); // 2
    System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}

private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
    // Get the INSTANCE field and make it accessible
    Field field = SingletonClass.class.getDeclaredField("INSTANCE");
    field.setAccessible(true);

    // Remove the final modifier
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    // Set new value
    field.set(null, newInstance);
}

Die letzten beiden Zeilen der main () -Methode stimmen nicht mit dem Wert von INSTANCE überein. Ich vermute, dass JIT die Methode vollständig entfernt hat, da das Feld statisch final ist. Durch Entfernen des letzten Schlüsselworts gibt der Code korrekte Werte aus.

Wenn Sie Ihr Mitgefühl (oder das Fehlen davon) für Singletons beiseite lassen und für eine Minute vergessen, dass die Verwendung einer solchen Reflexion Ärger bereitet - ist meine Annahme richtig, dass JIT-Optimierungen schuld sind? Wenn ja - sind diese nur auf statische Endfelder beschränkt?

Kelm
quelle
1
Ein Singleton ist eine Klasse, für die nur eine Instanz existieren kann. Daher haben Sie keinen Singleton, sondern nur eine Klasse mit einem static finalFeld. Außerdem spielt es keine Rolle, ob dieser Reflection-Hack aufgrund von JIT oder Parallelität unterbrochen wird.
Holger
@Holger Dieser Hack wurde in Unit-Tests nur durchgeführt, um den Singleton für mehrere Testfälle einer Klasse zu verspotten, die ihn verwendet. Ich sehe nicht ein, wie Parallelität es verursacht haben könnte (es gibt keine im obigen Code) und ich würde wirklich gerne wissen, was passiert ist.
Kelm
1
Nun, Sie sagten in Ihrer Frage "hochkonkurrierende Singleton-Klasse" und ich sage " es ist egal ", was es kaputt macht. Wenn also Ihr spezieller Beispielcode aufgrund von JIT unterbrochen wird und Sie eine Problemumgehung dafür finden, ändert sich der tatsächliche Code von Unterbrechung aufgrund von JIT zu Unterbrechung aufgrund von Parallelität. Was haben Sie gewonnen?
Holger
@Holger okay, der Wortlaut war dort etwas zu stark, tut mir leid. Was ich damit gemeint habe war Folgendes: Wenn wir nicht verstehen, warum etwas so schrecklich schief geht, neigen wir dazu, in Zukunft von der gleichen Sache gebissen zu werden. Deshalb möchte ich lieber den Grund kennen, als anzunehmen, dass "es einfach passiert". Wie auch immer, danke, dass du dir die Zeit genommen hast zu antworten!
Kelm

Antworten:

7

Nehmen Sie Ihre Frage wörtlich: „ … ist meine Annahme richtig, dass JIT-Optimierungen schuld sind? ”, Die Antwort lautet ja, es ist sehr wahrscheinlich, dass die JIT-Optimierungen in diesem speziellen Beispiel für dieses Verhalten verantwortlich sind.

Da das Ändern von static finalFeldern jedoch völlig außerhalb der Spezifikation liegt, gibt es andere Dinge, die es ähnlich brechen können. Zum Beispiel hat das JMM keine Definition für die Speichersichtbarkeit solcher Änderungen, daher ist es völlig unbestimmt, ob oder wann andere Threads solche Änderungen bemerken. Sie müssen es nicht einmal konsistent bemerken, dh sie können den neuen Wert verwenden, gefolgt von der erneuten Verwendung des alten Werts, selbst wenn Synchronisationsprimitive vorhanden sind.

Allerdings sind JMM und Optimierer hier ohnehin schwer zu trennen.

Ihre Frage „ … sind diese nur auf statische Endfelder beschränkt? ”Ist viel schwieriger zu beantworten, da Optimierungen natürlich nicht auf static finalFelder beschränkt sind, aber das Verhalten von z. B. nicht statischen finalFeldern nicht dasselbe ist und auch Unterschiede zwischen Theorie und Praxis aufweist.

Bei nicht statischen finalFeldern sind unter bestimmten Umständen Änderungen über Reflection zulässig. Dies wird durch die Tatsache angezeigt, dass dies setAccessible(true)ausreicht, um eine solche Änderung zu ermöglichen, ohne sich in die FieldInstanz zu hacken , um das interne modifiersFeld zu ändern .

Die Spezifikation sagt:

17.5.3. Nachträgliche Änderung von finalFeldern

In einigen Fällen, z. B. bei der Deserialisierung, muss das System die finalFelder eines Objekts nach der Erstellung ändern . finalFelder können durch Reflexion und andere implementierungsabhängige Mittel geändert werden. Das einzige Muster, in dem dies eine vernünftige Semantik hat, ist eines, in dem ein Objekt konstruiert wird und dann die finalFelder des Objekts aktualisiert werden. Das Objekt sollte weder für andere Threads sichtbar gemacht noch die finalFelder gelesen werden, bis alle Aktualisierungen der finalFelder des Objekts abgeschlossen sind. Einfrieren eines finalFeldes tritt sowohl am Ende des Konstruktors, in dem das finalFeld eingestellt ist, als auch unmittelbar nach jeder Änderung eines finalFeldes durch Reflexion oder einen anderen speziellen Mechanismus auf.

Ein weiteres Problem besteht darin, dass die Spezifikation eine aggressive Optimierung von finalFeldern ermöglicht. Innerhalb eines Threads ist es zulässig, Lesevorgänge eines finalFeldes mit den Änderungen eines finalFeldes neu anzuordnen , die nicht im Konstruktor stattfinden.

Beispiel 17.5.3-1. Aggressive Optimierung von finalFeldern
class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // uses reflection to change a.x to 2 
    } 
}

In der dMethode darf der Compiler die Lesevorgänge xund den Aufruf gfrei neu anordnen. So new A().f()könnte zurückkehren -1, 0oder 1.

In der Praxis ist die Ermittlung der richtigen Stellen, an denen aggressive Optimierungen möglich sind, ohne die oben beschriebenen rechtlichen Szenarien zu verletzen, ein offenes Problem . Sofern -XX:+TrustFinalNonStaticFieldsnicht anders angegeben, optimiert die HotSpot-JVM nicht statische finalFelder nicht wie static finalFelder.

Wenn Sie das Feld nicht als deklarieren final, kann die JIT natürlich nicht davon ausgehen, dass es sich niemals ändert. Wenn jedoch keine Grundelemente für die Thread-Synchronisierung vorhanden sind, werden möglicherweise die tatsächlichen Änderungen im optimierten Codepfad (einschließlich des) berücksichtigt reflektierende). Daher kann es den Zugriff immer noch aggressiv optimieren, aber nur so, als ob die Lese- und Schreibvorgänge immer noch in der Programmreihenfolge innerhalb des ausführenden Threads erfolgen. Sie würden die Optimierungen also nur bemerken, wenn Sie sie von einem anderen Thread aus ohne geeignete Synchronisationskonstrukte betrachten.

Holger
quelle
Es scheint, dass viele Leute versuchen, dieses finals auszunutzen , aber obwohl sich einige als leistungsfähiger erwiesen haben, nslohnt es sich nicht, viele andere Codes zu beschädigen. Grund, warum Shenandoah zum Beispiel auf einigen seiner Flaggen zurückweicht
Eugene