Какую гарантию видимости получают обычные записи, выполненные до записи в volatile-поле, после чтения этого поля другим потоком?
Если поток прочитал значение, записанное в volatile-поле другим потоком, все обычные записи, выполненные до volatile-записи, становятся ему видимыми. Это правило задаёт отношение happens-before: volatile-запись синхронизируется с последующим чтением того же поля.
Гарантия действует только для записей, предшествующих volatile-записи. volatile не делает составные операции атомарными и не распространяет автоматически записи, выполненные после неё.
Многопоточность требует не только атомарности операций, но и определённых правил видимости изменений между потоками. Без формальной модели компилятор, процессор и кэш процессора могли бы переупорядочивать операции так, что наблюдаемое поведение зависело бы от реализации и аппаратной архитектуры.
Java Memory Model, формализованная в Java 5, описывает такие гарантии через отношения happens-before. Для volatile-полей был закреплён лёгкий механизм публикации изменений без блокировки: запись имеет семантику release, а чтение — семантику acquire.
Пусть поток сначала подготавливает несколько обычных полей объекта, а затем публикует флаг готовности через volatile-поле. Другой поток читает этот флаг и видит признак готовности, но без правил JMM он не обязан был бы увидеть актуальные значения обычных полей.
Неправильное понимание механизма приводит к двум типичным ошибкам: отказу от синхронизации там, где нужна атомарность, и попытке использовать volatile-запись до подготовки данных. В результате можно получить потерянные обновления, чтение устаревшего состояния или наблюдение объекта в неподготовленном виде.
Обычные действия потока, расположенные до записи в volatile-поле, происходят раньше этой записи. Если другой поток читает это поле и получает значение, опубликованное данной записью, volatile-запись synchronizes-with чтением. Через транзитивность happens-before предыдущие обычные записи становятся видимыми читателю.
Например, запись data = 42 перед публикацией ready = true будет видима потоку, который увидел ready == true благодаря этой публикации:
Чтение ready не просто сообщает значение флага: оно устанавливает границу видимости для действий, предшествовавших volatile-записи. Однако записи, выполненные после ready = true, этой гарантией не покрываются.
volatile не превращает выражение вроде увеличения счётчика в неделимую операцию. Последовательность «прочитать, прибавить, записать» может выполняться конкурентно несколькими потоками; для неё нужны AtomicInteger, блокировка или другой подход. Кроме того, volatile-публикация безопасна только при корректном жизненном цикле данных: читатель должен действительно увидеть соответствующее значение volatile-поля.
В сервисе поток загрузки конфигурации заполнял обычный объект настроек, а затем выставлял volatile-флаг loaded. Потоки запросов проверяли этот флаг перед использованием настроек. Такой вариант был выбран вместо блокировки, потому что конфигурация публиковалась один раз, а чтения происходили часто; после публикации объект не изменялся.
Рассматривалась публикация ссылки через обычное поле, но она не давала безопасного распространения состояния объекта. Другим вариантом была блокировка при каждом чтении: она обеспечивала бы корректность, но добавляла ненужные расходы и усложняла горячий путь. Итоговое решение с volatile-флагом было корректным именно потому, что все поля сначала заполнялись, затем выполнялась volatile-запись, а после публикации состояние оставалось неизменным.
Если бы конфигурация продолжала изменяться, одного флага было бы недостаточно. Тогда потребовались бы неизменяемые снимки с безопасной публикацией, атомарная замена ссылки на снимок либо явная синхронизация обновлений и чтений.
Нет. Гарантия распространяется на действия до volatile-записи, но не на последующие изменения. Если объект изменяется после публикации, каждое такое изменение должно иметь собственный механизм публикации: volatile-доступ, блокировку, атомарную структуру или неизменяемую замену объекта.
Volatile обеспечивает согласованные правила видимости и порядок volatile-доступов, но не означает, что поток немедленно выполнит новое чтение или что произвольный цикл без чтения поля заметит изменение. После фактического чтения volatile-поля поток не может произвольно игнорировать установленный JMM порядок. Для ожидания изменения всё равно нужен корректный цикл чтения, а для остановки потока часто предпочтительнее механизм с блокированием или уведомлением.
Иногда безопасная публикация возможна через специальные правила JMM, например для корректно сконструированных final-полей, но это не делает произвольную публикацию изменяемого объекта безопасной. Сам факт завершения конструктора не заменяет синхронизацию между потоками. Для общего изменяемого состояния нужны явная безопасная публикация и механизм координации последующих изменений.