Al jugar con las pruebas unitarias para una clase singleton altamente concurrente, me topé con el siguiente comportamiento extraño (probado en 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);
}
Las últimas 2 líneas del método main () no están de acuerdo con el valor de INSTANCE: supongo que JIT eliminó el método por completo ya que el campo es estático final. Al eliminar la palabra clave final, el código genera valores correctos.
Dejando a un lado su simpatía (o falta de ella) por los singletons y olvidando por un minuto que usar una reflexión como esta es un problema, ¿es correcto asumir que las optimizaciones JIT son las culpables? Si es así, ¿están limitados solo a campos finales estáticos?
java
reflection
java-8
jit
Kelm
fuente
fuente
static final
campo. Además de eso, no importa si este truco de reflexión se rompe debido a JIT o concurrencia.Respuestas:
Tomando su pregunta literalmente, “ … ¿es correcta mi suposición en que las optimizaciones JIT son las culpables? ", La respuesta es sí, es muy probable que las optimizaciones JIT sean responsables de este comportamiento en este ejemplo específico.
Pero dado que cambiar los
static final
campos está completamente fuera de especificación, hay otras cosas que pueden romperlo de manera similar. Por ejemplo, el JMM no tiene una definición para la visibilidad de la memoria de dichos cambios, por lo tanto, no se especifica por completo si otros subprocesos notan dichos cambios. Ni siquiera se les exige que lo noten de manera consistente, es decir, pueden usar el nuevo valor, seguido de usar el valor anterior nuevamente, incluso en presencia de primitivas de sincronización.Sin embargo, el JMM y el optimizador son difíciles de separar de todos modos aquí.
Su pregunta " ... ¿están limitados a los campos finales estáticos solamente? "Es mucho más difícil de responder, ya que las optimizaciones, por supuesto, no se limitan a los
static final
campos, pero el comportamiento de, por ejemplo, losfinal
campos no estáticos , no es el mismo y también tiene diferencias entre la teoría y la práctica.Para
final
campos no estáticos , se permiten modificaciones a través de Reflection en ciertas circunstancias. Esto se indica por el hecho de quesetAccessible(true)
es suficiente para hacer posible tal modificación, sin hackear laField
instancia para cambiar elmodifiers
campo interno .La especificación dice:
En la práctica, determinar los lugares correctos donde las optimizaciones agresivas son posibles sin romper los escenarios legales descritos anteriormente, es un problema abierto , por lo que, a menos que
-XX:+TrustFinalNonStaticFields
se haya especificado, el HotSpot JVM no optimizará losfinal
campos no estáticos de la misma manera que losstatic final
campos.Por supuesto, cuando no declara el campo como
final
, el JIT no puede suponer que nunca cambiará, aunque, en ausencia de primitivas de sincronización de subprocesos, puede considerar las modificaciones reales que ocurren en la ruta de código que optimiza (incluido el reflexivos). Por lo tanto, aún puede optimizar agresivamente el acceso, pero solo como si las lecturas y escrituras aún ocurrieran en el orden del programa dentro del hilo en ejecución. Por lo tanto, solo notaría las optimizaciones al mirarlo desde un hilo diferente sin construcciones de sincronización adecuadas.fuente
final
, pero, aunque algunos han demostrado tener un mejor rendimiento, algunos ahorrosns
no valen la pena romper muchos otros códigos. Razón por la cual Shenandoah está retrocediendo en algunas de sus banderas, por ejemplo