Возможно ли потерять обновления, если общий счётчик объявлен volatile, но несколько потоков увеличивают его?
Да, обновления могут потеряться. volatile гарантирует видимость последнего записанного значения и определённый порядок доступа к этой переменной, но не делает составную операцию «прочитать, увеличить, записать» атомарной.
Для безопасного счётчика нужен механизм, объединяющий чтение и изменение: AtomicInteger, блокировка или другой подход, соответствующий сценарию нагрузки.
Многопоточные программы выполняются на процессорах и виртуальных машинах, где чтения, записи и переупорядочивание операций могут отличаться от интуитивной последовательности исходного текста. Одной видимости значения недостаточно, если несколько потоков одновременно изменяют общий объект.
Java Memory Model разделяет задачи видимости, упорядочивания и атомарности. Модификатор volatile предназначен прежде всего для безопасной публикации изменений между потоками без взаимного исключения, но не заменяет синхронизацию составных операций.
Увеличение счётчика состоит из трёх логических действий: поток читает текущее значение, вычисляет новое и записывает результат. Если два потока прочитали одно и то же значение, каждый вычислил одинаковый следующий результат, а затем записал его, одна инкрементация будет потеряна.
Например, при начальном значении 0 оба потока могут прочитать 0 и записать 1. Итог окажется равен 1, хотя выполнены две операции увеличения.
В выражении value++ volatile-чтение и volatile-запись действительно выполняются с необходимыми гарантиями видимости, но между ними другой поток может изменить значение. Ни одна из этих гарантий не превращает всю последовательность в неделимую операцию.
Для счётчика обычно применяют AtomicInteger с атомарной операцией увеличения. Внутри такая операция использует механизм сравнения и условной замены: значение заменяется только если оно осталось прежним; при конфликте поток повторяет попытку с актуальным значением.
Если изменение включает несколько полей или должно сохранять сложный инвариант, лучше использовать synchronized или Lock. Блокировка защищает критическую секцию целиком, но может привести к ожиданию потоков и снижению пропускной способности.
Для высококонкурентного счётчика статистики часто подходит LongAdder. Он уменьшает конкуренцию, распределяя обновления по внутренним ячейкам, однако получение итогового значения не является снимком, согласованным с одной точкой времени, и структура сложнее.
Важно различать атомарность отдельной операции и точность всей бизнес-логики. Даже атомарное увеличение не защищает связку из нескольких независимых действий, если между ними требуется единый инвариант.
Сервис собирает число обработанных запросов. Сначала разработчик использовал обычное поле и получил гонку данных. Затем поле объявили volatile: наблюдение значения стало более предсказуемым, но итоговая статистика всё ещё занижалась при нагрузочном тестировании, потому что инкременты конфликтовали.
Вариант с synchronized дал корректный результат, но стал узким местом при большом числе коротких операций. Вариант с AtomicLong сохранил точность и обычно обеспечил достаточную производительность, поэтому его выбрали для счётчика, значение которого часто читается.
Для метрики, где важнее пропускная способность записи, чем мгновенно согласованное чтение, рассмотрели LongAdder. Его выбрали бы при подтверждённой высокой конкуренции и допустимой семантике приблизительного текущего значения; для точного счётчика завершённых операций оставили AtomicLong.
Для переменной volatile чтение и запись имеют специальные гарантии модели памяти, но это не означает атомарность произвольной последовательности операций. Кроме того, нельзя делать общий вывод о составных действиях вроде проверки условия с последующим изменением: граница атомарности проходит по отдельной операции доступа, а не по нескольким инструкциям бизнес-логики.
Не обязательно. Отдельные атомарные вызовы могут быть корректны сами по себе, но между ними другой поток способен изменить значение. Если проверка лимита и изменение должны образовывать одну неделимую операцию, нужна подходящая атомарная операция сравнения-и-замены либо блокировка, охватывающая весь инвариант.
Нельзя оценивать замену по одному чтению. Все обращения к общему состоянию должны быть согласованы с единым протоколом синхронизации: если запись выполняется под блокировкой, а чтение происходит без неё, корректность зависит от дополнительных гарантий публикации и всё равно не решает проблему составных операций. Если выбран synchronized, защищаемые чтения и записи обычно должны использовать тот же монитор.