Я смотрю на пример кода от "Параллелизма Java на практике" Brian Goetz. Он говорит, что возможно, что этот код останется в бесконечном цикле, потому что "значение 'готовых' никогда не могло бы становиться видимым к потоку читателя". Я не понимаю, как это может произойти...
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;
}
}
Поскольку готов
не помечен как volatile
, и значение может быть кэшировано в начале цикла while
, потому что он не изменяется в цикле while
. Это один из способов, которыми джиттер оптимизирует код.
Таким образом, возможно, что поток запускается до ready = true
и читает ready = false
кэширует этот поток локально и никогда не читает это снова.
Проверьте ключевое слово volatile .
Причина объясняется в разделе, следующем за примером кода.
3.1.1 Устаревшие данные
NoVisibility
продемонстрировал один из способов, которыми недостаточно синхронизированные программы могут вызывают удивительные результаты: устаревшие данные . Когда поток чтения проверяетготов
, он может увидеть устаревшее значение. Если синхронизация не используется каждый раз при доступе к переменной , можно увидеть устаревшее значение для этой переменной.
Модель памяти Java позволяет JVM выполнять оптимизировать обращения к ссылкам, например, если это однопоточное приложение, если только поле не помечено как volatile
или доступы с удерживаемой блокировкой (на самом деле история немного усложняется с блокировками).
В приведенном вами примере JVM может сделать вывод, что поле ready
не может быть изменено в текущем потоке, поэтому она заменит ! ready
на false
, что приведет к бесконечному циклу. Пометка поля как изменчивое
заставит JVM каждый раз проверять значение поля (или, по крайней мере, гарантировать, что изменения готовы
распространяются на работающий поток).
private static boolean ready;
private static int number;
способ работы модели памяти состоит в том, что каждый поток может читать и записывать в свою собственную копию этих переменных (проблема также затрагивает нестатические переменные-члены). Это следствие того, как может работать базовая архитектура.
Джереми Мэнсон и Брайан Гетц :
В многопроцессорных системах процессоры обычно имеют один или несколько уровней кеш-памяти, что повышает производительность как за счет ускорения доступа. к данным (поскольку данные находятся ближе к процессору) и уменьшению трафика на шине общей памяти (поскольку многие операции с памятью могут выполняться локальными кэшами). Кеши памяти могут значительно повысить производительность, но они создают множество новых проблем. Что, например, происходит, когда два процессора одновременно проверяют одно и то же место в памяти? При каких условиях они будут видеть одно и то же значение?
Итак, в вашем примере два потока могут работать на разных процессорах, каждый с копией готовой
в своих собственных отдельных кэшах. Язык Java предоставляет механизмы volatile
и synchronized
для обеспечения того, чтобы значения, видимые потоками, были синхронизированы.
Проблема кроется в аппаратном обеспечении - каждый ЦП ведет себя по-разному в отношении согласованности кэша, видимости памяти и переупорядочения операций. Java здесь лучше, чем C ++, потому что он определяет кроссплатформенную модель памяти, на которую могут рассчитывать все программисты. Когда Java работает в системе, модель памяти которой слабее, чем требуется моделью памяти Java, JVM должна компенсировать разницу.
Такие языки, как C, «наследуют» модель памяти базового оборудования. Ведется работа по созданию формальной модели памяти C ++, чтобы программы на C ++ могли означать одно и то же на разных платформах.