Разберите ситуацию: один поток изменяет обычное поле, затем освобождает конкретный lock, а другой после захвата того же lock читает это поле. Какие гарантии даёт Java Memory Model?
Освобождение lock одним потоком происходит до последующего успешного захвата того же lock другим потоком в терминах happens-before. Поэтому второй поток гарантированно увидит изменения, выполненные первым до освобождения lock, даже если поле не объявлено volatile.
Это гарантирует видимость и порядок наблюдения, но не делает операции над полем атомарными вне защищённой критической секции. Если какой-либо доступ к полю выполняется без того же lock, общая гарантия корректности нарушается.
Java Memory Model формализует взаимодействие потоков с памятью: видимость изменений, допустимые перестановки операций и правила публикации данных. Без такой модели компилятор, процессор и кэш процессора могли бы оптимизировать выполнение так, что результат зависел бы от платформы и конкретного запуска.
Механизм happens-before нужен для выражения переносимых гарантий между потоками. Синхронизация через монитор стала одним из базовых способов одновременно упорядочить действия и обеспечить видимость общего состояния.
Пусть поток обновляет несколько связанных полей состояния, а другой поток принимает решение на основании этих значений. Если между ними нет корректного отношения happens-before, читающий поток может увидеть старые значения или несовместимое состояние из-за кэширования и перестановки операций.
Использование одного и того же lock устраняет этот риск только для участков, которые действительно защищены им. Если запись выполнена под lock, а чтение — без lock, JMM не обязана предоставлять такую же гарантию видимости.
Для встроенного монитора действует правило: unlock на мониторе happens-before последующего lock на этом же мониторе. Все записи, выполненные первым потоком до освобождения lock, становятся наблюдаемыми для второго потока после успешного захвата этого lock.
Поле value не обязано быть volatile, потому что и запись, и чтение защищены одним монитором. Кроме видимости, монитор обеспечивает взаимоисключение: одновременно выполнять эти критические секции сможет только один поток.
Важно, что отношение относится к конкретному объекту-монитору. Синхронизация на разных объектах, даже если они логически связаны, не создаёт нужного отношения happens-before. Также захват lock должен быть успешным: попытка ожидания или работа с другим механизмом сама по себе не заменяет корректную синхронизацию.
Lock не превращает любую последовательность действий с полем в атомарную. Например, операция чтение-модификация-запись требует, чтобы вся последовательность выполнялась под тем же lock; отдельная синхронизация чтения и записи может не защитить её от гонки.
Компромисс заключается в стоимости и конкуренции: блокировка упрощает reasoning о состоянии, но может уменьшить параллелизм и привести к ожиданию. Для простого независимого флага иногда подходит volatile, а для составных инвариантов обычно нужен lock или специализированный класс из java.util.concurrent.
В сервисе один поток обновлял конфигурацию из внешнего хранилища, а рабочие потоки читали её во время обработки запросов. Сначала запись выполнялась под lock, но чтение было обычным доступом без синхронизации. На тестах проблема почти не проявлялась, однако при высокой нагрузке рабочий поток мог продолжать видеть старое состояние.
Рассматривались три варианта:
Выбрали третий вариант: новая конфигурация полностью строилась локально, затем публиковалась одной volatile-записью и больше не изменялась. Это устранило гонку, не добавило блокировок на пути чтения и сохранило согласованность снимка конфигурации.
Ответ: Нет, такой дизайн не даёт общей гарантии видимости. Правило happens-before связывает освобождение монитора с последующим захватом того же монитора, а не с произвольным чтением поля. Читающий поток должен использовать тот же lock, volatile-механизм, подходящий атомарный класс или другой корректный канал публикации.
Ответ: Нужного отношения happens-before не возникнет. JMM связывает операции только при выполнении определённых правил синхронизации; логическое назначение объектов не имеет значения. В результате второй поток может не увидеть запись, даже если оба участка формально находятся внутри synchronized-блоков.
Ответ: Нет. Взаимоисключение действует только между потоками, использующими тот же механизм синхронизации. Несинхронизированное изменение может пересекаться с защищённой операцией, поэтому возникают потерянные обновления и гонка данных; для счётчика следует либо единообразно использовать lock, либо выбрать подходящий класс, например AtomicInteger.