Что делает volatile и что такое happens-before? Почему volatile не заменяет синхронизацию?
Короткий ответ
- volatile гарантирует видимость: запись видна последующим чтениям из других потоков
- Запрещает кэширование значения в регистрах и переупорядочивание вокруг
- happens-before — отношение порядка в Java Memory Model
- Запись volatile happens-before последующего чтения той же переменной
- volatile не даёт атомарности составных операций вроде i++
- Для счётчиков — AtomicInteger, для сложных инвариантов — локи
volatile решает проблему видимости и упорядочивания, но не атомарности — для этого нужны Atomic-классы или блокировки.
Как сказать вслух
пример ответаvolatile гарантирует, что запись в переменную увидят другие потоки, и запрещает процессору и компилятору переставлять операции вокруг неё. Формально это описывается через happens-before: всё, что поток сделал до записи volatile, станет видно потоку, который потом её прочитал. Но атомарности это не даёт: инкремент — это чтение плюс запись, и два потока могут потерять обновление. Для таких случаев я возьму AtomicInteger или блокировку.
Подробный ответ
Основной ответ
Java Memory Model допускает, что без синхронизации поток может не увидеть изменения другого потока (значение в кэше/регистре) и что операции переупорядочиваются. volatile даёт две гарантии: видимость (чтение возвращает последнюю запись) и упорядочивание — запись volatile нельзя переставить с предшествующими операциями, чтение — с последующими. happens-before — формальное отношение: если A happens-before B, результаты A видны в B. Его создают volatile (запись → чтение), вход/выход из synchronized, Thread.start/join, операции java.util.concurrent. При этом volatile не делает составные операции атомарными: count++ — это read-modify-write, и обновления теряются. Типичные применения volatile — флаг остановки потока и safe publication неизменяемого состояния.
Ключевые моменты
- Видимость. Без volatile/синхронизации цикл while(!stopped) может никогда не увидеть изменение флага.
- happens-before. Запись volatile и последующее чтение связывают всю предшествующую работу потоков, не только саму переменную.
- Нет атомарности. i++ на volatile теряет обновления; нужен AtomicInteger (CAS) или lock.
- Где уместен. Флаги, одиночная публикация ссылки на готовый объект, double-checked locking для синглтона.
Практический контекст
Вопрос отделяет тех, кто писал конкурентный код, от тех, кто читал про него. На практике — флаги graceful shutdown, конфиг, перечитываемый на лету, DCL-синглтон. Интервьюер часто докручивает: «а инкремент?», «а два volatile-поля с инвариантом между ними?» — правильный ход мысли: видимость есть, атомарности и согласованности нескольких полей нет, берём Atomic или lock.
Пример кода
class Worker implements Runnable {
private volatile boolean stopped = false;
public void stop() { stopped = true; } // видно потоку run()
@Override public void run() {
while (!stopped) {
// без volatile JIT может вынести проверку из цикла
doWork();
}
}
private void doWork() { /* ... */ }
}Частые ошибки
- Считают volatile заменой synchronized и делают volatile-счётчики
- Объясняют volatile только «чтением из главной памяти», не упоминая запрет переупорядочивания
- Не могут назвать ни одного источника happens-before кроме synchronized