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?
la source
static final
champ. En plus de cela, peu importe si ce hack de réflexion se brise en raison de JIT ou de la concurrence.Réponses:
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 final
champs, mais le comportement, par exemple desfinal
champs non statiques , n'est pas le même et présente également des différences entre la théorie et la pratique.Pour les
final
champs non statiques , les modifications via Reflection sont autorisées dans certaines circonstances. Ceci est indiqué par le fait qu'ilsetAccessible(true)
suffit de rendre possible une telle modification, sans pirater l'Field
instance pour changer lemodifiers
champ interne .La spécification dit:
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:+TrustFinalNonStaticFields
contraire, la machine virtuelle Java HotSpot n'optimisera pas lesfinal
champs non statiques de la même manière que lesstatic final
champs.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.la source
final
s, mais, bien que certains se soient révélés plus performants, économiser certainsns
ne vaut pas la peine de casser beaucoup d'autres codes. Raison pour laquelle Shenandoah recule sur certains de ses drapeaux par exemple