Ломка JIT-оптимизаций с отражением - PullRequest
9 голосов
/ 07 января 2020

При работе с модульными тестами для высококонкурентного одноэлементного класса я наткнулся на следующее странное поведение (проверено на 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);
}

Последние 2 строки метода main () не согласен со значением INSTANCE - я предполагаю, что JIT полностью избавился от метода, так как поле stati c final. Удаление последнего ключевого слова приводит к тому, что код выводит правильные значения.

Оставляя в стороне ваше сочувствие (или его отсутствие) к одиночкам и на минуту забывая, что использование отражения, как это, вызывает проблемы - мое предположение правильно в этом JIT оптимизация виновата? Если да - ограничены ли они только stati c final поля?

1 Ответ

7 голосов
/ 08 января 2020

В буквальном смысле ваш вопрос: «… верно ли мое предположение в том, что виноваты JIT-оптимизации? », ответ - да, очень вероятно, что JIT-оптимизации ответственны за такое поведение в этой спецификации. c пример.

Но так как изменение полей static final полностью не соответствует спецификации, существуют другие вещи, которые могут нарушить его аналогичным образом. Например, JMM не имеет определения видимости таких изменений в памяти, следовательно, совершенно не определено, замечают ли другие потоки такие изменения или нет. Они даже не обязаны замечать это последовательно, то есть они могут использовать новое значение, а затем снова использовать старое значение, даже при наличии примитивов синхронизации.

Хотя JMM и оптимизатор трудно В любом случае, разделите здесь.

Ваш вопрос «… это те вопросы, которые ограничены только stati c только конечными полями? », ответить на него гораздо труднее, поскольку оптимизации, конечно, не ограничиваются 1012 * полей, но поведение, например, нестатических c final полей, не одинаково и имеет различия между теорией и практикой.

Для не статичных c final поля, модификации через Reflection допускаются при определенных обстоятельствах. На это указывает тот факт, что setAccessible(true) достаточно, чтобы сделать возможной такую ​​модификацию, без взлома экземпляра Field для изменения внутреннего поля modifiers.

В спецификации говорится :

17.5.3. Последующая модификация final полей

В некоторых случаях, таких как десериализация, системе потребуется изменить поля final объекта после построения. final поля могут быть изменены с помощью отражения и других зависящих от реализации средств. Единственный шаблон, в котором это имеет разумную семантику, это шаблон, в котором объект создается, а затем обновляются поля final объекта. Объект не должен быть видимым для других потоков, а также поля final не должны читаться, пока все обновления полей final объекта не будут завершены. Замораживание поля final происходит как в конце конструктора, в котором установлено поле final, так и сразу после каждой модификации поля final с помощью отражения или другого специального механизма.

Другая проблема заключается в том, что спецификация допускает агрессивную оптимизацию полей final. Внутри потока допустимо переупорядочивать чтение поля final с теми модификациями поля final, которые не имеют места в конструкторе.

Пример 17.5.3-1. Агрессивная оптимизация final полей
class 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 
    } 
}

В методе d компилятору разрешено свободно изменять порядок чтения x и вызова g. Таким образом, new A().f() может вернуть -1, 0 или 1.

На практике, определение правильных мест, где возможна агрессивная оптимизация, не нарушая юридические сценарии ios, описанные выше, являются открытым вопросом , поэтому, если не указано -XX:+TrustFinalNonStaticFields, JVM HotSpot не будет оптимизировать не * stati c final поля так же, как static final fields.

Конечно, когда вы не объявляете поле как final, JIT не может предполагать, что оно никогда не изменится, хотя, в отсутствие примитивов синхронизации потоков, он может учитывать фактические изменения, происходящие в пути кода, который он оптимизирует (включая отражающие). Таким образом, он все еще может активно оптимизировать доступ, но только , как если бы чтения и записи, все еще происходили в порядке программы в потоке выполнения. Таким образом, вы заметите оптимизации только при взгляде на него из другого потока без правильных конструкций синхронизации.

...