Gymterview
middle

В чём различия между 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. Прочитать текущее значение переменной.
  2. Вычислить новое значение.
  3. Записать новое значение только если текущее значение не изменилось с момента чтения. Если изменилось – повторить с шага 1.

Это позволяет обойтись без блокировок (lock-free), но при высокой конкуренции может вызвать «спин» – поток многократно повторяет попытки CAS.

Аналогия из жизни. volatile – это табло на стене: все видят актуальное число, но если два человека одновременно хотят увеличить число, один из них «затрёт» обновление другого. AtomicInteger – это табло с контролем: человек берёт текущее число, вычисляет новое и перевешивает табличку, но только если никто не успел изменить число до него. Если кто-то уже изменил – он смотрит заново и повторяет попытку.

На собеседовании. Ключевой момент: объясните, что volatile достаточно для простых флагов (один пишет – все читают), но для счётчиков и других read-modify-write операций нужны Atomic*-классы. Бонусный ответ: упомяните LongAdder (Java 8+), который при высокой конкуренции работает быстрее AtomicLong за счёт разделения значения на ячейки (striped).