Rompre les optimisations JIT avec la réflexion

9

En jouant avec des tests unitaires pour une classe singleton hautement simultanée, je suis tombé sur le comportement étrange suivant (testé sur 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);
}

Les 2 dernières lignes de la méthode main () ne sont pas d'accord sur la valeur d'INSTANCE - je suppose que JIT s'est débarrassé complètement de la méthode puisque le champ est statique final. La suppression du mot clé final rend les valeurs de sortie correctes du code.

Laissant de côté votre sympathie (ou son absence) pour les singletons et oubliant pendant une minute que l'utilisation de la réflexion comme celle-ci pose problème - est-ce que mon hypothèse est correcte en ce sens que les optimisations JIT sont à blâmer? Si tel est le cas - ceux-ci sont-ils limités aux champs finaux statiques uniquement?

Kelm
la source
1
Un singleton est une classe pour laquelle une seule instance peut exister. Par conséquent, vous n'avez pas de singleton, vous avez juste une classe avec un static finalchamp. En plus de cela, peu importe si ce hack de réflexion se brise en raison de JIT ou de la concurrence.
Holger
@Holger ce hack a été effectué dans des tests unitaires uniquement dans le but de se moquer du singleton pour plusieurs cas de test d'une classe qui l'utilise. Je ne vois pas comment la concurrence pourrait avoir causé cela (il n'y en a pas dans le code ci-dessus) et j'aimerais vraiment savoir ce qui s'est passé.
Kelm
1
Eh bien, vous avez dit «classe singleton hautement concurrente» dans votre question et je dis « ça n'a pas d'importance » ce qui la fait casser. Donc, si votre exemple de code particulier rompt en raison de JIT et que vous trouvez une solution pour cela, puis, le vrai code passe de la rupture en raison de JIT à la rupture en raison de la concurrence, qu'avez-vous gagné?
Holger
@ Holger d'accord, la formulation était un peu trop forte là-bas, désolé pour ça. Ce que je voulais dire était ceci - si nous ne comprenons pas pourquoi quelque chose va si horriblement mal, nous sommes enclins à être mordus par la même chose à l'avenir, alors je préfère en connaître la raison plutôt que de supposer que "ça arrive juste". Quoi qu'il en soit, merci d'avoir pris le temps de répondre!
Kelm

Réponses:

7

Prenant votre question au pied de la lettre, « … mon hypothèse est-elle correcte dans la mesure où les optimisations JIT sont à blâmer? », La réponse est oui, il est très probable que les optimisations JIT soient responsables de ce comportement dans cet exemple spécifique.

Mais depuis le changement static final champs est complètement hors spécifications, il y a d'autres choses qui peuvent le casser de la même manière. Par exemple, le JMM n'a pas de définition pour la visibilité de la mémoire de tels changements, par conséquent, il est complètement non spécifié si ou quand d'autres threads remarquent de tels changements. Ils ne sont même pas tenus de le noter de manière cohérente, c'est-à-dire qu'ils peuvent utiliser la nouvelle valeur, puis réutiliser l'ancienne valeur, même en présence de primitives de synchronisation.

Cependant, le JMM et l'optimiseur sont difficiles à séparer de toute façon ici.

Votre question « … sont-elles limitées aux champs finaux statiques uniquement? »Est beaucoup plus difficile à répondre, car les optimisations ne sont bien sûr pas limitées aux static finalchamps, mais le comportement, par exemple des finalchamps non statiques , n'est pas le même et présente également des différences entre la théorie et la pratique.

Pour les finalchamps non statiques , les modifications via Reflection sont autorisées dans certaines circonstances. Ceci est indiqué par le fait qu'il setAccessible(true)suffit de rendre possible une telle modification, sans pirater l' Fieldinstance pour changer le modifierschamp interne .

La spécification dit:

17.5.3. Modification ultérieure des finalchamps

Dans certains cas, comme la désérialisation, le système devra modifier les finalchamps d'un objet après la construction. finalles champs peuvent être modifiés via la réflexion et d'autres moyens dépendant de l'implémentation. Le seul modèle dans lequel cela a une sémantique raisonnable est celui dans lequel un objet est construit et ensuite les finalchamps de l'objet sont mis à jour. L'objet ne doit pas être rendu visible aux autres threads, ni les finalchamps doivent être lus, jusqu'à ce que toutes les mises à jour des finalchamps de l'objet sont terminées. Les blocages d'un finalchamp se produisent à la fois à la fin du constructeur dans lequel le finalchamp est défini et immédiatement après chaque modification d'un finalchamp via la réflexion ou un autre mécanisme spécial.

Un autre problème est que la spécification permet une optimisation agressive des finalchamps. Au sein d'un thread, il est possible de réorganiser les lectures d'un finalchamp avec les modifications d'un finalchamp qui n'ont pas lieu dans le constructeur.

Exemple 17.5.3-1. Optimisation agressive des finalchamps
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 
    } 
}

Dans la dméthode, le compilateur est autorisé à réorganiser les lectures xet l'appel à glibrement. Ainsi new A().f()pourrait revenir -1, 0ou 1.

En pratique, déterminer les bons endroits où des optimisations agressives sont possibles sans casser les scénarios légaux décrits ci-dessus, est un problème ouvert , donc, sauf indication -XX:+TrustFinalNonStaticFieldscontraire, la machine virtuelle Java HotSpot n'optimisera pas les finalchamps non statiques de la même manière que les static finalchamps.

Bien sûr, lorsque vous ne déclarez pas le champ comme final, le JIT ne peut pas supposer qu'il ne changera jamais, cependant, en l'absence de primitives de synchronisation de thread, il peut considérer les modifications réelles qui se produisent dans le chemin de code qu'il optimise (y compris le réfléchissants). Ainsi, il peut toujours optimiser agressivement l'accès, mais seulement comme si les lectures et les écritures se produisaient toujours dans l'ordre du programme dans le thread d'exécution. Vous ne remarquerez donc les optimisations que lorsque vous le regardez à partir d'un thread différent sans constructions de synchronisation appropriées.

Holger
la source
il semble que beaucoup de gens essaient d'exploiter ce finals, mais, bien que certains se soient révélés plus performants, économiser certains nsne vaut pas la peine de casser beaucoup d'autres codes. Raison pour laquelle Shenandoah recule sur certains de ses drapeaux par exemple
Eugene