Есть ли предел для переопределения конечного статического поля с помощью Reflection?
В некоторых из моих модульных тестов я столкнулся со странным поведением с отражением в конечном статическом поле. Ниже приведен пример, иллюстрирующий мою проблему.
У меня есть базовый класс Singleton, который содержит целое число
public class BasicHolder {
private static BasicHolder instance = new BasicHolder();
public static BasicHolder getInstance() {
return instance;
}
private BasicHolder() {
}
private final static Integer VALUE = new Integer(0);
public Integer getVALUE() {
return VALUE;
}
}
Мой тестовый пример состоит в цикле и установке с помощью Reflection the VALUE индекса итерации, а затем утверждение, что VALUE по праву равно индексу итерации.
class TestStaticLimits {
private static final Integer NB_ITERATION = 10_000;
@Test
void testStaticLimit() {
for (Integer i = 0; i < NB_ITERATION; i++) {
setStaticFieldValue(BasicHolder.class, "VALUE", i);
Assertions.assertEquals(i, BasicHolder.getInstance().getVALUE(), "REFLECTION DID NOT WORK for iteration "+i);
System.out.println("iter " + i + " ok" );
}
}
private static void setStaticFieldValue(final Class obj, final String fieldName, final Object fieldValue) {
try {
final Field field = obj.getDeclaredField(fieldName);
field.setAccessible(true);
final Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, fieldValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error while setting field [" + fieldName + "] on object " + obj + " Message " + e.getMessage(), e);
}
}
}
Результат довольно удивителен, потому что он не постоянен, мой тест не проходит около итерации ~ 1000, но кажется, что он никогда не бывает одинаковым.
Кто-нибудь уже сталкивался с этой проблемой?
Ответы
Ответ 1
В JLS упоминается, что изменение окончательных полей после построения проблематично - см. 17.5. окончательная семантика поля
Поля, объявленные как окончательные, инициализируются один раз, но никогда не меняются при обычных обстоятельствах. Подробная семантика конечных полей несколько отличается от семантики нормальных полей. В частности, компиляторы имеют большую свободу перемещать чтения конечных полей через барьеры синхронизации и вызовы произвольных или неизвестных методов. Соответственно, компиляторам разрешается сохранять значение конечного поля, кэшированного в регистре, и не перезагружать его из памяти в ситуациях, когда необходимо перезагружать неконечное поле.
и 17.5.3. Последующая модификация финальных полей:
Другая проблема заключается в том, что спецификация допускает агрессивную оптимизацию конечных полей. Внутри потока допустимо переупорядочивать операции чтения конечного поля с теми модификациями конечного поля, которые не имеют места в конструкторе.
В дополнение к этому JavaDocs Field.set также включает предупреждение об этом:
Установка конечного поля таким способом имеет смысл только во время десериализации или восстановления экземпляров классов с пустыми конечными полями, прежде чем они станут доступны для доступа другими частями программы. Использование в любом другом контексте может привести к непредсказуемым последствиям, включая случаи, когда другие части программы продолжают использовать исходное значение этого поля.
Похоже, что здесь мы наблюдаем, что JIT использует возможности переупорядочения и кэширования, предоставляемые Спецификацией языка.
Ответ 2
Это из-за оптимизации JIT. Чтобы доказать это, отключите его, используя следующую опцию VM
:
-Djava.compiler=NONE
В этом случае все 10_000
итераций будут работать.
Или исключите метод BasicHolder.getVALUE
из компиляции:
-XX:CompileCommand=exclude,src/main/BasicHolder.getVALUE
Что на самом деле происходит под капотом, так это то, что после nth
итерации горячий метод getVALUE
компилируется и static final Integer VALUE
агрессивно оптимизируется (это действительно постоянная времени) 1. С этого момента утверждение начинает проваливаться.
Вывод -XX:+PrintCompilation
с моими комментариями:
val 1 # System.out.println("val " + BasicHolder.getInstance().getVALUE());
val 2
val 3
...
922 315 3 src.main.BasicHolder::getInstance (4 bytes) # Method compiled
922 316 3 src.main.BasicHolder::getVALUE (4 bytes) # Method compiled
...
val 1563 # after compilation
val 1563
val 1563
val 1563
...
1 - Анатомический парк JVM: Константы точно в срок.