Розбиття оптимізацій JIT з відображенням


9

Під час тестування з одиничними тестами для високоспорідненого однотонного класу я натрапив на таке дивне поведінку (тестоване на 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 рядки основного () методу не розходяться щодо значення INSTANCE - я здогадуюсь, що JIT повністю позбувся методу, оскільки поле є статичним остаточним. Видалення остаточного ключового слова призводить до правильних значень коду.

Залишаючи осторонь свою симпатію (або її відсутність) до одинаків і на хвилину забувши, що, використовуючи роздуми, подібні до цього, є клопот - чи моє припущення правильне в тому, що в цьому винні оптимізації JIT? Якщо так - чи обмежені лише статичні кінцеві поля?


1
Синглтон - це клас, для якого може існувати лише один екземпляр. Тому у вас немає синглів, у вас просто клас з static finalполем. Крім того, не має значення, чи зламається ця рефлексійна хака через JIT або одночасність.
Холгер

@Holger цей злом робився в одиничних тестах лише як спроба знущатися з одинарного для кількох тестових випадків класу, який його використовує. Я не бачу, як сумісність могла це спричинити (в коді цього немає жодного), і я дуже хотів би знати, що сталося.
Келм

1
Ну, ви сказали у своєму запитанні "дуже одночасний однокласний клас", і я кажу " не має значення ", що змушує його зламатись. Отже, якщо ваш конкретний приклад код зламається через JIT, і ви виявите, що для цього вирішується, а потім реальний код змінюється від розриву через JIT до злому через одночасність, що ви отримали?
Холгер

@Holger добре, формулювання там було занадто сильне, вибачте з цього приводу. Що я мав на увазі, це було таке: якщо ми не розуміємо, чому щось настільки жахливо не так, ми в майбутньому схильні отримувати одне і те саме, тож я скоріше знаю причину, ніж припускати, що "це просто відбувається". У будь-якому випадку, дякую, що знайшли свій час для відповіді!
Келм

Відповіді:


7

Якщо поставити своє запитання буквально, « … чи моє припущення правильне в тому, що винні оптимізації JIT? ", Відповідь" так ", дуже ймовірно, що оптимізація JIT є відповідальною за таку поведінку в цьому конкретному прикладі.

Але оскільки зміна static finalполів повністю не відповідає специфікації, є й інші речі, які можуть її порушити аналогічно. Наприклад, у JMM немає визначення видимості пам'яті таких змін, отже, не визначено, чи помічають інші зміни чи коли інші потоки. Вони навіть не зобов’язані це послідовно помічати, тобто вони можуть використовувати нове значення з подальшим використанням старого значення ще раз за наявності примітивів синхронізації.

Хоча JMM та оптимізатор тут важко розділити.

Ваше запитання “ … чи обмежені лише статичні кінцеві поля? ”Відповісти набагато важче, оскільки оптимізація, звичайно, не обмежується static finalполями, але поведінка, наприклад, нестатичних finalполів, не є однаковою і має відмінності між теорією та практикою.

Для нестатичних 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.

На практиці визначення правильних місць, де можлива агресивна оптимізація без порушення законних сценаріїв, описаних вище, є відкритим питанням , тому, якщо -XX:+TrustFinalNonStaticFieldsне буде вказано, JVM HotSpot не буде оптимізувати нестатичні finalполя так само, як і static finalполя.

Звичайно, коли ви не оголошуєте поле як final, JIT не може припустити, що воно ніколи не зміниться, хоча, за відсутності примітивів синхронізації потоків, він може врахувати фактичні зміни, що відбуваються в оптимізованому шляху коду (включаючи рефлексивні). Таким чином, він все ще може агресивно оптимізувати доступ, але лише так, якщо читання і запис все-таки відбувається в програмному порядку в виконуваному потоці. Таким чином, ви помітили б оптимізацію лише при перегляді на неї з іншого потоку без належних конструкцій синхронізації.


Здається, багато людей намагаються використовувати це final, але, хоча деякі з них виявилися ефективнішими, деякі заощаджувати nsне варто, порушуючи багато інших кодів. Причина, чому Шенандоа відступає на деяких своїх прапорах, наприклад
Євген
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.