В чём различия между volatile и Atomic переменными?
volatile
Модификатор volatile гарантирует видимость изменений переменной для всех потоков и запрет переупорядочивания, но не гарантирует атомарность составных операций. Операция count++ над volatile int count по-прежнему состоит из трёх шагов (чтение текущего значения, инкремент, запись нового значения), и между ними другой поток может вмешаться.
Atomic-переменные
Классы пакета java.util.concurrent.atomic (такие как AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference и другие) обеспечивают атомарность составных операций за счёт использования низкоуровневых CAS-инструкций (Compare-And-Swap) процессора. Они также гарантируют видимость, как и volatile.
Сравнение
| Характеристика | volatile |
Atomic* |
|---|---|---|
| Видимость | Да | Да |
| Атомарность чтения/записи | Да (для примитивов и ссылок) | Да |
| Атомарность read-modify-write | Нет | Да (getAndIncrement(), compareAndSet() и т.д.) |
| Механизм | Memory barrier | CAS (Compare-And-Swap) без блокировок |
| Блокировка потока | Нет | Нет (lock-free) |
| Подходит для счётчиков | Нет (race condition) | Да |
| Подходит для флагов | Да | Избыточно (достаточно volatile) |
Пример: небезопасный volatile-счётчик vs безопасный AtomicInteger
Пример: сравнение volatile и AtomicInteger
public class VolatileVsAtomic {
private volatile int volatileCounter = 0;
private final AtomicInteger atomicCounter = new AtomicInteger(0);
// НЕ потокобезопасно! count++ — не атомарная операция
public void unsafeIncrement() {
volatileCounter++;
}
// Потокобезопасно: CAS-операция атомарна
public void safeIncrement() {
atomicCounter.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
VolatileVsAtomic demo = new VolatileVsAtomic();
int threadCount = 100;
int incrementsPerThread = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
demo.unsafeIncrement();
demo.safeIncrement();
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// volatile: скорее всего < 100_000 (потеряны инкременты)
System.out.println("volatile: " + demo.volatileCounter);
// atomic: всегда ровно 100_000
System.out.println("atomic: " + demo.atomicCounter.get());
}
}
Как работает CAS
Операция Compare-And-Swap выполняется на уровне процессора за одну инструкцию:
- Прочитать текущее значение переменной.
- Вычислить новое значение.
- Записать новое значение только если текущее значение не изменилось с момента чтения. Если изменилось – повторить с шага 1.
Это позволяет обойтись без блокировок (lock-free), но при высокой конкуренции может вызвать «спин» – поток многократно повторяет попытки CAS.
Аналогия из жизни.
volatile– это табло на стене: все видят актуальное число, но если два человека одновременно хотят увеличить число, один из них «затрёт» обновление другого.AtomicInteger– это табло с контролем: человек берёт текущее число, вычисляет новое и перевешивает табличку, но только если никто не успел изменить число до него. Если кто-то уже изменил – он смотрит заново и повторяет попытку.
На собеседовании. Ключевой момент: объясните, что
volatileдостаточно для простых флагов (один пишет – все читают), но для счётчиков и других read-modify-write операций нужныAtomic*-классы. Бонусный ответ: упомянитеLongAdder(Java 8+), который при высокой конкуренции работает быстрееAtomicLongза счёт разделения значения на ячейки (striped).