Короткий ответ
volatile решает проблему видимости: чтение и запись volatile-переменной выполняются напрямую из основной памяти, а между записью одним потоком и чтением другим устанавливается отношение happens-before. Однако volatile не обеспечивает атомарность: составная операция «прочитать — изменить — записать» вроде counter++ остаётся неатомарной. Атомарные классы из java.util.concurrent.atomic дают атомарные операции без блокировок — на базе аппаратной инструкции CAS.
Как это работает подробнее
volatile идеален для флагов состояния вроде boolean running, которые один поток выставляет, а другой читает. При counter++ два потока могут одновременно прочитать старое значение и записать результат, «потеряв» одно из увеличений, — даже если поле объявлено volatile.
Атомарные классы AtomicInteger, AtomicLong, AtomicReference предоставляют методы getAndIncrement(), addAndGet(), compareAndSet(expected, newValue), реализованные на аппаратной инструкции CAS (Compare-And-Swap). CAS действует оптимистично: compareAndSet проверяет, равно ли текущее значение ожидаемому, и только тогда обменивает его на новое; если значение уже изменил другой поток, операция не выполняется и поток повторяет попытку.
Пример кода
import java.util.concurrent.atomic.AtomicInteger;
class VolatileVsAtomic {
private volatile int count; // видимость есть, атомарности нет
private final AtomicInteger atomicCount = new AtomicInteger(0);
void demo() {
count++; // не атомарно: возможны потерянные обновления
atomicCount.incrementAndGet(); // атомарно, без блокировок, на CAS
}
}Что использовать на практике
- volatile — для простых флагов состояния (running, ready), где нужна только видимость свежего значения.
- Атомарные классы — для счётчиков и составных операций вроде инкремента, когда важна масштабируемость без блокирующего ожидания.
- compareAndSet(expected, new) — основа оптимистичных алгоритмов с повторной попыткой при конкуренции.
Подводные камни
- Не делай
counter++по volatile-полю: «прочитать-изменить-записать» не атомарно, обновления теряются. - volatile не заменяет synchronized, когда составная операция затрагивает несколько полей.
- CAS-циклы при высокой конкуренции дают много неудачных попыток (spinning) и тратят CPU.
Как отвечать на собеседовании
- Сформулируй главное различие: volatile — про видимость, атомарные классы — про атомарность операций → покажи, что count++ не атомарен даже с volatile → приведи AtomicInteger.incrementAndGet() на CAS → правило выбора: флаг — volatile, счётчик — AtomicInteger.