Вопрос о "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;
}
}
Ответы
Ответ 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
.