Подтвердить что ты не робот

Вопрос о "Java Concurrency на практике"

Я просматриваю образец кода из "Java Concurrency in Practice" Брайана Гетца. Он говорит, что возможно, что этот код останется в бесконечном цикле, потому что "значение" готово "никогда не станет видимым для потока читателя". Я не понимаю, как это может произойти...

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    } 
}
4b9b3361

Ответ 1

Поскольку ready не помечен как volatile, и значение может быть кэшировано в начале цикла while, потому что оно не изменяется в цикле while. Это один из способов оптимизации дрожания кода.

Итак, возможно, что поток начинается до ready = true и читает ready = false кэширует этот поток локально и никогда не читает его снова.

Отметьте ключевое слово volatile.

Ответ 2

Причина объясняется в разделе, следующем за примером с образцом кода.

3.1.1 Устаревшие данные

NoVisibility продемонстрировал, как недостаточно синхронизированные программы могут вызвать неожиданные результаты: устаревшие данные. Когда поток читателя проверяет ready, он может видеть устаревшее значение. Если синхронизация не используется каждый раз при доступе к переменной, можно увидеть устаревшее значение для этой переменной.

Ответ 3

Модель памяти Java позволяет JVM оптимизировать обратные обращения и, например, если это однопоточное приложение, если поле не помечено как volatile или доступ с блокировкой (история немного усложняется блокировки на самом деле).

В приведенном примере JVM может сделать вывод, что поле ready не может быть изменено в текущем потоке, поэтому оно заменило бы !ready на false, вызывая бесконечный цикл. Маркировка поля как volatile заставит JVM каждый раз проверять значение поля (или, по крайней мере, гарантировать, что изменения ready распространяются на текущий поток).

Ответ 4

Проблема кроется в аппаратном обеспечении - каждый процессор имеет другое поведение в отношении согласованности кеша, видимости памяти и переупорядочения операций. Java здесь лучше, чем С++, потому что он определяет кросс-платформенную модель памяти, на которую могут рассчитывать все программисты. Когда Java работает в системе, модель памяти которой слабее той, которая требуется для модели памяти Java, JVM должна внести свой вклад.

Языки, подобные C "наследуют" модель памяти базового оборудования. Есть работа, чтобы дать С++ формальную модель памяти, чтобы программы на С++ могли означать одно и то же на разных платформах.

Ответ 5

private static boolean ready;
private static int number;

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

Джереми Мэнсон и Брайан Гетц:

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

Итак, в вашем примере два потока могут выполняться на разных процессорах, каждая из которых имеет копию ready в своих собственных отдельных кэшах. Язык Java предоставляет механизмы volatile и synchronized для обеспечения синхронизации значений, наблюдаемых потоками.

Ответ 6

public class NoVisibility {

    private static boolean ready = false;
    private static int number;

    private static class ReaderThread extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        number = 42;
        Thread.sleep(20000);
        ready = true;
    }
}

Поместите вызов Thread.sleep() в течение 20 секунд, что произойдет, когда JIT заработает в течение этих 20 секунд, и он оптимизирует проверку и кэширует значение или вообще удаляет это условие. И поэтому код не будет отображаться.

Чтобы остановить это, вы ДОЛЖНЫ использовать volatile.