Скорее всего, будут различия в реализации кэша процессора на двух разных аппаратных устройствах, которые вы используете.Наверное, совсем не JVM.
Согласованность памяти - довольно сложная тема, я рекомендую проверить учебник, подобный this , для более углубленного изучения.Также см. Этот объяснитель модели памяти Java для получения подробной информации о гарантиях, которые JVM предоставит, независимо от вашего оборудования.
Я объясню гипотетический сценарий, в которомповедение, которое вы наблюдали, могло произойти, не зная конкретных деталей вашего чипсета:
ГИПОТЕТИЧЕСКИЙ СЦЕНАРИЙ
Два потока: Ваш «поток пользовательского интерфейса» (скажем, этоработает на ядре 1) и "фоновом потоке" (ядро 2).Вашей переменной completed
назначается одна фиксированная ячейка памяти во время компиляции (предположим, что мы разыменовали this
и т. Д., И мы установили, что это за местоположение).completed
представлен одним байтом, начальное значение «0».
Поток пользовательского интерфейса на ядре 1 быстро достигает цикла занятости-ожидания.В первый раз, когда он пытается прочитать completed
, возникает «ошибка кэша».Таким образом, запрос отправляется через в кэш и читает completed
(вместе с другими 31 байтами в строке ) из основной памяти.Теперь, когда строка кэша находится в кеше L1 ядра 1, она считывает значение и обнаруживает, что это «0».(Ядра не подключены напрямую к основной памяти; они могут получить к ней доступ только через кеш.) Итак, ожидание занятости продолжается;ядро 1 запрашивает одно и то же место в памяти, completed
, снова и снова, но вместо пропуска кэша L1 теперь может удовлетворить каждый запрос и больше не нуждается в связи с основной памятью.
Между тем, вклВ ядре 2 фоновый поток работает для завершения вызова API.В конце концов он завершает работу и пытается записать «1» в ту же ячейку памяти, completed
.Опять же, происходит сбой кэша, и происходит то же самое.Ядро 2 записывает «1» в соответствующее место в своем собственном кэше L1.Но эта строка кэша еще не обязательно записывается обратно в основную память.Даже если бы это было так, ядро 1 все равно не ссылается на основную память, поэтому оно не увидит изменений.Затем ядро 2 завершает поток, возвращается и уходит, чтобы выполнить работу где-то еще.
(К тому времени, когда ядро 2 назначено другому процессу, его кэш, вероятно, синхронизирован с основной памятью и очищен.Итак, «1» возвращается в основную память. Не то, чтобы это имело какое-либо значение для ядра 1, которое продолжает работать исключительно из своего кэша L1.)
И все продолжается таким образом, пока что-то не произойдетслучается, чтобы предложить кэшу ядра 1, что он грязный, и он должен обновить.Как я упоминал в комментариях, это может быть fence , возникающий как часть вызова System.out.println()
, записи отладчика и т. Д. Естественно, если бы вы использовали блок synchronized
, компиляторпоместили забор в ваш собственный код.
TAKEAWAYS
... и поэтому вы всегда защищаете доступ к общим переменным с помощью блока synchronized
!(Таким образом, вам не нужно тратить дни на чтение руководств процессора, пытаясь понять детали модели памяти на конкретном оборудовании, которое вы используете, просто для того, чтобы разделить байт информации между двумя потоками.) Ключевое слово volatile
также будетрешить проблему, но см. некоторые ссылки в статье Jenkov для сценариев, в которых этого недостаточно.