Есть ли предел для переопределения конечного статического поля с помощью 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: Константы точно в срок.