← Назад к списку
ТехническаяJava и KotlinMiddle

Что делает 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

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru