When fiddling with unit tests for a highly-concurrent singleton class I stumbled upon the following weird behaviour (tested on 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);
}
The last 2 lines of the main() method disagree on the value of INSTANCE - my guess is that JIT got rid of the method completely since the field is static final. Removing the final keyword makes the code output correct values.
Leaving aside your sympathy (or lack thereof) for singletons and forgetting for a minute that using reflection like this is asking for trouble - is my assumption correct in that JIT optimisations are to blame? If so - are those limited to static final fields only?
Taking your question literally, “…is my assumption correct in that JIT optimizations are to blame?”, the answer is yes, it’s very likely that the JIT optimizations are responsible for this behavior in this specific example.
But since changing static final
fields is completely off specification, there are other things that can break it similarly. E.g. the JMM has no definition for the memory visibility of such changes, hence, it is completely unspecified whether or when other threads notice such changes. They are not even required to notice it consistently, i.e. they may use the new value, followed by using the old value again, even in the presence of synchronization primitives.
Though, the JMM and the optimizer are hard to separate anyway here.
Your question “…are those limited to static final fields only?” is much harder to answer, as optimizations are, of course, not limited to static final
fields, but the behavior of, e.g. non-static final
fields, is is not the same and has differences between theory and practice as well.
For non-static final
fields, modifications via Reflection are allowed in certain circumstances. This is indicated by the fact that setAccessible(true)
is enough to make such modification possible, without hacking into the Field
instance to change the internal modifiers
field.
The specification says:
17.5.3. Subsequent Modification of
final
FieldsIn some cases, such as deserialization, the system will need to change the
final
fields of an object after construction.final
fields can be changed via reflection and other implementation-dependent means. The only pattern in which this has reasonable semantics is one in which an object is constructed and then thefinal
fields of the object are updated. The object should not be made visible to other threads, nor should thefinal
fields be read, until all updates to thefinal
fields of the object are complete. Freezes of afinal
field occur both at the end of the constructor in which thefinal
field is set, and immediately after each modification of afinal
field via reflection or other special mechanism.…
Another problem is that the specification allows aggressive optimization of
final
fields. Within a thread, it is permissible to reorder reads of afinal
field with those modifications of afinal
field that do not take place in the constructor.Example 17.5.3-1. Aggressive Optimization offinal
Fieldsclass 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 the
d
method, the compiler is allowed to reorder the reads ofx
and the call tog
freely. Thus,new A().f()
could return-1
,0
, or1
.
In practice, determining the right places where aggressive optimizations are possible without breaking the legal scenarios described above, are an open issue, so unless -XX:+TrustFinalNonStaticFields
has been specified, the HotSpot JVM will not optimize non-static final
fields the same way as static final
fields.
Of course, when you don’t declare the field as final
, the JIT can not assume that it will never change, though, in the absence of thread synchronization primitives, it may consider the actual modifications happening in the code path it optimizes (including the reflective ones). So it may still aggressively optimize the access, but only as-if the reads and writes still happen in the program order within the executing thread. So you’d only notice the optimizations when looking at it from a different thread without proper synchronization constructs.