Какой тип оптимизации JIT применяется для цикла while - PullRequest
2 голосов
/ 22 декабря 2019

Я записал этот код:

public class Main {

    private boolean stopThread = false;
    private int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        final Main main = new Main();

        new Thread(() -> {
            try {
                System.out.println("Start");
                Thread.sleep(100);
                System.out.println("Done");
                main.stopThread = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            while (!main.stopThread) {
                main.counter++;
            }
            System.out.println(main.counter);
        }).start();
        System.out.println("End");
    }
}

, и когда я его запусту, цикл while будет работать вечно. Я немного боролся с этим, и я запутался, какой тип оптимизации JIT применяется к этому коду.

Сначала я подумал, что это проблема с видимостью переменной stopThread, но даже если онаэто правда, цикл while должен остановиться немного позже, чем я назначил stopThread на true (когда кэш процессора из 1-го потока был сброшен в основную память), поэтому это не может быть так. Похоже, что переменная JIT жестко закодирована от false до stopThread, и если это правда, почему эта переменная не обновляется периодически как-то во время выполнения?

Очевидно, что ключевое слово volatile исправило это, но оно не отвечает на этот вопрос, потому что volatile может обеспечить видимость, а также предотвратить JIT от ряда оптимизаций.

Более того, когда я изменяю время sleep на 1 мс, второй поток завершается правильно, поэтому я почти уверен, что речь не идет о переменной видимости.

ОБНОВЛЕНИЕ : Стоит отметить, что я получаю ненулевое значение из counter, когда время sleep установлено в 1-10 мс.

ОБНОВЛЕНИЕ 2 : Кроме того, я могу сказать, что -XX:+PrintCompilation показывает, что в случае, если время sleep установлено равным 100 мс, цикл while компилируется, а On Stack Replacement происходит.

ОБНОВЛЕНИЕ 3 : Вероятно, это то, что я искал: https://www.youtube.com/watch?v=ADxUsCkWdbE&feature=youtu.be&t=889. И, как я думал, это одна из «оптимизаций», выполняемых JIT, и способ предотвратить это - указать переменную как volatile, или введите loadloadFence в качестве первой строки в цикле while.

ОТВЕТ : Как сказал @apangin:

Эта оптимизация является Loopинвариантный подъемJIT разрешено перемещать нагрузку stopThread из цикла, поскольку он может предполагать, что энергонезависимое поле не изменяется внешне, и JIT также видит, что stopThread не изменяется внутри цикла.

Ответы [ 3 ]

5 голосов
/ 22 декабря 2019

Причина не в оптимизации JIT. Ваш код выполняет несинхронизированный доступ к общей переменной, stopThread (назовем это "флагом").

По существу, существует условие гонки между потоком, который устанавливает флаг вистинное значение и другой поток, который проверяет значение. Если перед установкой цикла флаг установлен в значение true, код завершается. Если нет (гонка проиграна), цикл будет продолжаться неопределенно долго, потому что в кэше ЦП хранится значение Falsey. Когда флаг равен volatile, его значение считывается из основной памяти вместо кэша ЦП, и цикл в конечном итоге заканчивается сразу после того, как поток установки флага завершается со сном.

3 голосов
/ 22 декабря 2019

Я думаю, что вы решаете эту проблему с неправильной стороны.

У вас есть программа с неопределенным поведением в отношении видимости памяти. Если два потока читают и записывают совместно используемую переменную, это должно быть сделано особым образом, чтобы гарантировать, что один поток видит записи других потоков. Вам необходимо использовать переменную synchronized или volatile, чтобы гарантировать, что запись происходит до чтения.

Если происходит до того, как не будет,JVM может предполагать, что только текущий поток обращается к / обновляет переменную. То, что на самом деле делает JIT-компилятор, зависит от всевозможных вещей, и не стоит вдаваться в подробности.

Стоит также отметить, что volatile не обязательно является лучшим (наиболее эффективным) исправлением длянедостаток видимости памяти.

Список литературы:

0 голосов
/ 22 декабря 2019

без какой-либо синхронизации или ключевого слова volatile, разрешено не читать переменную stopThread из памяти, она может читать ее из кэша (или даже из регистра), куда она помещает ее в первый раз.

Что именно она сделала ипочему это работает со сном (1), мы можем только догадываться. Я предполагаю, что первый поток написал stopThread = true, прежде чем второй поток прочитал его. Возможно, сначала попробуйте запустить цикл.

...