Почему volatile отличается от модели взаимодействия с кучей
Что проверяют
Понимаете ли вы, что видимость и атомарность это разные гарантии, и знаете ли модель памяти Java хотя бы на уровне «почему поток не видит чужую запись». Для Android это повседневность: фоновые потоки и главный поток постоянно обмениваются флагами.
Как отвечать
Куча отвечает на вопрос «где лежат объекты», и она действительно общая для всех потоков. Но общая память сама по себе ничего не гарантирует: модель памяти разрешает компилятору, JIT и процессору переупорядочивать операции и держать значение поля в регистре. Поэтому поток способен вечно крутить while (running), читая давно устаревшее значение, хотя в куче лежит другое.
volatile (в Kotlin аннотация @Volatile) добавляет к полю две вещи:
- отношение happens-before: запись в volatile-поле предшествует последующему чтению этого же поля, и вместе с ней становятся видны все записи, сделанные до неё;
- запрет на переупорядочивание вокруг обращения и на кэширование значения в регистре.
Чего он не даёт, назвать надо самому: атомарности составных операций. counter++ это чтение, увеличение и запись, между ними вклинивается другой поток, и здесь нужен AtomicInteger или синхронизация. Короткая формулировка отличия: куча это место хранения, volatile это правила видимости, synchronized добавляет к видимости ещё и взаимное исключение.
Где спотыкаются
Объясняют volatile как «чтение напрямую из оперативной памяти в обход кэша». Кэши процессора и так когерентны, речь про барьеры памяти и запрет оптимизаций, а не про поход мимо кэша.
Вешают volatile на счётчик или на проверку с последующим действием и получают гонку на ровном месте. Ленивая инициализация без блокировки ломается точно так же, одного модификатора для неё мало.
Считают, что volatile на ссылке защищает сам объект. Защищена только ссылка: то, что записали в объект до публикации, будет видно, а любые изменения его полей после этого снова требуют собственной синхронизации.