Для чего использовать volatile на 32-битной переменной
Что проверяют
Знаете ли вы модель памяти Java и понимаете ли, что атомарность и видимость это разные задачи. Упоминание 32 бит здесь подвох. Следом обычно спрашивают, почему volatile int не спасает count++ и что меняется для long.
Как отвечать
Сначала снимите подвох. По спецификации Java (JLS, раздел 17.7) чтение и запись int, float, boolean, ссылок и других типов до 32 бит атомарны всегда, с volatile или без. Разорванная запись возможна только у не-volatile long и double: JVM вправе записать их двумя половинами по 32 бита. Значит, на 32-битной переменной volatile ставят не ради атомарности, а ради двух других гарантий:
- видимость: запись в volatile-поле связана отношением happens-before с каждым последующим чтением этого поля. Без него JIT может вынести чтение из цикла, и
while (!stopped)не закончится никогда; - упорядочивание: всё, что поток записал до записи в volatile-поле, увидит другой поток, прочитавший это поле. На этом держатся безопасная публикация объекта и double-checked locking для синглтона.
В Kotlin то же самое делает аннотация @Volatile. На Android это особенно заметно: у ARM модель памяти слабее, чем у x86, и ошибки переупорядочивания, которые не воспроизводятся на ноутбуке, вылезают на телефоне.
Чего volatile не даёт: атомарности составных операций. counter++ это чтение, сложение и запись, и два потока спокойно потеряют инкремент. Для этого есть AtomicInteger и AtomicLong, synchronized, а в коде на корутинах часто проще держать состояние в MutableStateFlow и менять его через update { }.
Где спотыкаются
Говорят, что volatile делает операции атомарными. Атомарны только отдельные чтение и запись, инкремент остаётся гонкой.
Объясняют всё фразой «без volatile поток читает значение из своего кэша». Кэши процессоров когерентны, реальные причины в оптимизациях компилятора и JIT и в переупорядочивании записей.
Не знают про long и double. Как раз их возможный разрыв и есть причина, по которой в вопросе вообще упомянули 32 бита.