Является ли JIT причиной такого поведения?

Вдохновленный этим вопросом, я написал тест:

public class Main {

    private static final long TEST_NUMBERS = 5L;

    private static final long ITERATION_NUMBER = 100000L;

    private static long value;

    public static void main(final String [] args) throws Throwable {
        for(int i=0; i<TEST_NUMBERS; i++) {
            value = 0;
            final Thread incrementor = new Thread(new Incrementor());
            final Thread checker = new Thread(new Checker());
            incrementer.start();
            checker.start();
            checker.join();
            incrementer.join();
        }
    }

    static class Incrementor implements Runnable {
        public void run() {
            for(int i=0; i<ITERATION_NUMBER; i++){
                ++value;
            }
        }
    }

    static class Checker implements Runnable {
        public void run() {
            long nonEqualsCount = 0;
            for(int i=0; i<ITERATION_NUMBER; i++){
                if(value != value) {
                    ++nonEqualsCount;
                }
            }
            System.out.println("nonEqualsCount = " + nonEqualsCount);
        }
    }
}

Эта программа печатается в общем случае:

nonEqualsCount = 12; //or other non 0 value;
nonEqualsCount = 0;
nonEqualsCount = 0;
nonEqualsCount = 0;
nonEqualsCount = 0;

Первое: такое поведение я объясняю наличием JIT-компилятора. Значение кэша JIT-компилятора не volatile для каждого потока после "прогрева". Это правильно?

Второй: если первый прав или не прав, как я могу это проверить?

P.S. - Я знаю об опции PrintAssebly.

Обновление: среда: 64-разрядная версия Windows 7, JDK 1.7.0_40-b43 (горячая точка).


person Sergey Morozov    schedule 06.11.2013    source источник
comment
попробуйте с public void safePrintln (String s) { synchronized (System.out) { System.out.println (s); } }   -  person pvllnspk    schedule 06.11.2013
comment
@barn.gumbl: System.out синхронизирован. См. (PrintStream)[grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/   -  person Aaron Digulla    schedule 06.11.2013


Ответы (4)


То, что вы видите, вероятно, является артефактом JIT. Прежде чем он сработает, байт-код Java интерпретируется, что означает, что поток проверки может прерваться во время сравнения.

Кроме того, поскольку выполняется больше кода, повышается вероятность того, что кеши ЦП будут нуждаться в очистке.

Когда код оптимизируется с помощью JIT, он, вероятно, будет вставлять 64-битные операции, и, поскольку выполняется только небольшое количество кода, кеши больше не будут сбрасываться в основную память, что означает, что потоки не имеют возможности увидеть внесенные изменения. другим.

person Aaron Digulla    schedule 06.11.2013

Увеличение переменной long не является атомарным (размером 64 бита). В состоянии (value != value): может случиться так, что между чтением значения value первый поток может изменить значение. Тип volatile связан с visibility. Значения энергонезависимых переменных могут быть устаревшими. Так что ваш первый вывод кажется правильным.

person Areo    schedule 06.11.2013
comment
Если вы измените «long» на «int», этот вывод не изменится =). - person Sergey Morozov; 06.11.2013
comment
Я не говорил, что проблема с типом long. Проблема с неатомарной операцией. Увеличение int также не является атомарной операцией. :D - person Areo; 06.11.2013
comment
Истинный. Используйте абстрактный тип AtomicInteger или AtomicLong из пакета java.util.concurrent.atomic. - person mwhs; 06.11.2013
comment
@mwhs Почему это абстрактно? - person Boris the Spider; 06.11.2013
comment
Термин абстрактный не означает, что он абстрактен в смысле абстрактного класса в языке Java, а скорее означает, что он абстрактен в смысле типов данных. Объяснение см. в этой статье: en.wikipedia.org/wiki/Abstract_data_type - person mwhs; 06.11.2013

При первом проходе вашей программы эти утверждения могут быть правильными:

Этот код может продемонстрировать, что операция увеличения (++value) для переменной типа long (а также int) не является атомарной. Кроме того, это также может продемонстрировать, что операция != не является потокобезопасной, если она не используется в синхронизированном блоке. Но это не имеет ничего общего с используемым типом данных.

Ваше наблюдение, что что-то изменилось после первого прохода, также верно, но, например, если вы используете JVM Oracle/SUN, то реализация этого самого JIT-Complier ("Hotspot Engine") зависит от технической архитектуры, на которой он работает. .

Поэтому трудно сказать и проверить, что за это отвечает JIT-компилятор. Попытка вывести детали реализации движка JIT-Complier/Hotspot с использованием этого подхода — довольно эмпирический метод исследования. Ваше наблюдение может, например, измениться при переходе с Solaris на Windows.

Вот ссылка на детали реализации движка Hotspot: http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html

Чтобы получить дальнейшие эмпирические результаты, вы можете, например, попытаться вернуть JVM для работы в классическом режиме или уменьшить количество оптимизаций в JVM (режим клиента?). Если поведение изменится, это может быть еще одним показателем правильности вашей теории.

В любом случае: мне любопытно, каковы ваши выводы :-)

person mwhs    schedule 06.11.2013

Хотя вы правы в том, что это вызвано JIT, это не имеет ничего общего с volatile.

Некоторые JIT выполняют внутреннюю оптимизацию на лету и удаляют ненужный код для ускорения работы, и именно это происходит здесь. JIT определяет, что сравнение value != value всегда ложно, и полностью удаляет весь блок кода. Кроме того, он может определить, что этот цикл for теперь выполняется пустым, и также удалить весь цикл. В результате это будет окончательный оптимизированный класс проверки:

public void run() {
  System.out.println("nonEqualsCount = 0");
}

Вы можете убедиться в этом, измерив время, необходимое этому потоку для выполнения на каждом проходе. На первом проходе может потребоваться некоторое время для завершения, на втором это будет всего несколько наносекунд для println.

Примечание. Как правило, вы не можете ожидать, что JIT что-то сделает. В зависимости от фактической реализации, аппаратного обеспечения и других факторов он может оптимизировать или не оптимизировать ваш код. А если и оптимизируется, то результат точно так же невозможно определить, так как, например, код может быть оптимизирован гораздо раньше на медленном оборудовании, чем на быстром.

person TwoThe    schedule 06.11.2013
comment
Это означает, что при неправильном применении параллелизма вы (или JIT-компилятор) на самом деле создаете недетерминированный код? - person mwhs; 07.11.2013
comment
JIT всегда создает детерминированный код, но синхронизация параллельного выполнения по определению недетерминирована. Поэтому JIT даже пытаться не нужно. - person TwoThe; 07.11.2013