Вопрос 8 · Раздел 9

Что такое Atomic классы?

Обычная операция count++ не потокобезопасна:

Версии по языкам: English Russian Ukrainian

Junior уровень

Базовое понимание

Atomic классы — это набор классов из java.util.concurrent.atomic, которые обеспечивают потокобезопасные операции без блокировок (lock-free).

Lock-free = нет мьютекса, нет блокировки потока. Используется CAS-цикл: читаем значение, вычисляем новое, пытаемся записать через CAS. Если другой поток изменил значение — CAS возвращает false, повторяем цикл.

Зачем они нужны?

Обычная операция count++ не потокобезопасна:

// НЕ безопасно!
public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 3 операции: read → modify → write
        // Два потока могут прочитать одинаковое значение!
    }
}

Решение с Atomic

// Безопасно!
public class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Атомарная операция
    }
}

Основные Atomic классы

Класс Тип Пример использования
AtomicBoolean boolean Флаг состояния
AtomicInteger int Счётчик
AtomicLong long Счётчик (64-битный)
AtomicReference<T> Объект Потокобезопасная ссылка
AtomicIntegerArray int[] Массив счётчиков
AtomicLongAdder long Высоконагруженный счётчик

Примеры использования

// AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();   // ++i (возвращает новое значение)
counter.getAndIncrement();   // i++ (возвращает старое значение)
counter.addAndGet(5);        // i += 5
counter.compareAndSet(5, 10);// Если i==5, установить 10

// AtomicBoolean
AtomicBoolean flag = new AtomicBoolean(false);
flag.set(true);
boolean oldValue = flag.getAndSet(false);

// AtomicReference
AtomicReference<String> ref = new AtomicReference<>("initial");
ref.compareAndSet("initial", "updated");

Механизм: CAS + volatile

Atomic классы работают на двух столпах:

  1. volatile поле — гарантирует видимость изменений
  2. CAS (Compare-And-Swap) — атомарное обновление
// Упрощённая реализация AtomicInteger
public class AtomicInteger {
    private volatile int value; // volatile для видимости

    public final int incrementAndGet() {
        for (;;) { // Бесконечный цикл
            int current = get();         // 1. Читаем
            int next = current + 1;      // 2. Вычисляем
            if (compareAndSet(current, next)) { // 3. CAS — обновляем
                return next;             // Успех!
            }
            // CAS не удался — кто-то другой обновил, пробуем снова
        }
    }
}

Middle уровень

CAS (Compare-And-Swap) детально

CAS — это инструкция процессора, которая атомарно:

  1. Сравнивает значение по адресу с ожидаемым
  2. Если равно — записывает новое
  3. Если не равно — ничего не делает
// Псевдокод CAS
boolean CAS(memoryAddress, expectedValue, newValue) {
    if (*memoryAddress == expectedValue) {
        *memoryAddress = newValue;
        return true;
    }
    return false;
}

На x86 это инструкция LOCK CMPXCHG.

CMPXCHG (CoMPare and eXCHanGe) — x86-инструкция для CAS-операции. На ARM — ldxr/stxr, на RISC-V — lr.w/sc.w (Load Reserved / Store Conditional).

Обновление ссылок

public class AtomicReferenceDemo {
    private AtomicReference<List<String>> list = new AtomicReference<>(new ArrayList<>());

    public void addItem(String item) {
        list.updateAndGet(currentList -> {
            // Создаём новую копию (immutable update)
            List<String> newList = new ArrayList<>(currentList);
            newList.add(item);
            return newList;
        });
    }
}

VarHandle (Java 9+)

Начиная с Java 9, Atomic-классы переписаны с использованием VarHandle:

VarHandle (Java 9, JEP 193) — замена sun.misc.Unsafe для атомарных операций. Безопаснее (нет прямого доступа к памяти), но с теми же возможностями. API Atomic-классов остался обратно совместимым — код менять не нужно.

// Вместо sun.misc.Unsafe — типизированный и безопасный API
public class AtomicInteger {
    private static final VarHandle VALUE;
    private volatile int value;

    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }
}

Разные уровни гарантий памяти

AtomicLong counter = new AtomicLong(0);

// Разные методы записи:
counter.set(100);          // Volatile запись — полная видимость
counter.lazySet(100);      // Ослабленная видимость: не ставит StoreLoad barrier, запись может стать видна с задержкой. Быстрее!
counter.compareAndSet(0, 100); // CAS с полным барьером
Метод Гарантия Скорость
set(v) Полная видимость сразу Стандарт
lazySet(v) Видимость eventual Быстрее
compareAndSet Полный барьер Стандарт
weakCompareAndSet Может вернуть false spuriously (даже если значение совпадает) на некоторых архитектурах. Это не баг — разрешено спецификацией для оптимизации на CPU с слабой моделью памяти (ARM, RISC-V). Быстрее

LongAdder для высокой конкуренции

При сотнях потоков, обновляющих один счётчик, AtomicLong начинает тормозить (CAS постоянно фейлится).

// ПЛОХО при высокой конкуренции:
AtomicLong counter = new AtomicLong(0);
// Каждый поток постоянно "промахивается" в CAS

// ХОРОШО при высокой конкуренции:
LongAdder counter = new LongAdder();
counter.increment(); // Распределяет по ячейкам
long total = counter.sum(); // Суммирует все ячейки

Как работает LongAdder:

  • Вместо одной переменной — массив ячеек (Cells)
  • Разные потоки обновляют разные ячейки
  • При sum() значения суммируются

Когда Atomic классы НЕ подходят

  1. Составные операции над несколькими полями: нужно атомарно обновить два поля — AtomicInteger не поможет, нужен synchronized или AtomicReference с immutable-объектом
  2. Высокая конкуренция (100+ потоков): CAS-цикл тратит CPU впустую — лучше LongAdder
  3. Долгие вычисления между read и write: если между чтением и записью проходит много времени — CAS будет постоянно fail-ить, лучше Lock

Senior уровень

Under the Hood: Реализация CAS

На уровне x86 процессора:

; LOCK CMPXCHG — атомарная операция
; rax = expected, rcx = newValue, [memory] = actual value

lock cmpxchg [memory], rcx
; Если [memory] == rax:
;   [memory] = rcx
;   ZF = 1 (success)
; Иначе:
;   rax = [memory]
;   ZF = 0 (failure)

Префикс lock блокирует шину памяти на время операции, обеспечивая атомарность на многопроцессорных системах.

ABA Проблема

Классическая проблема CAS:

Поток 1: прочитал значение A
Поток 2: изменил A → B → A (значение вернулось!)
Поток 1: CAS(A, new_value) — УСПЕХ! Но состояние менялось!

Решение: AtomicStampedReference

// Хранит значение + версию (штамп)
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

int[] stampHolder = new int[1];
String value = ref.get(stampHolder);
int stamp = stampHolder[0];

// CAS с проверкой версии
boolean success = ref.compareAndSet("A", "B", stamp, stamp + 1);

AtomicMarkableReference

// Хранит значение + boolean флаг
AtomicMarkableReference<Node> ref = new AtomicMarkableReference<>(node, false);

// Пометить узел как "удалённый"
ref.compareAndSet(node, node, false, true);

LongAdder внутреннее устройство

// Упрощённо
public class LongAdder {
    // Базовое значение (используется при низкой конкуренции)
    volatile long base;

    // Массив ячеек (используется при высокой конкуренции)
    volatile Cell[] cells;

    static final class Cell {
        @Contended // Padding для избежания False Sharing
        volatile long value;
    }

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // Базовый CAS не удался — используем Cell
            // Каждый поток хешируется в свою ячейку
            // ...
        }
    }

    public long sum() {
        long sum = base;
        for (Cell a : cells) {
            if (a != null) sum += a.value;
        }
        return sum;
    }
}

Производительность и Highload

Бенчмарк (примерный, 100 потоков)

Операция Время на операцию Примечание
synchronized ~5000 ns Context switching
AtomicLong ~100 ns CAS contention
LongAdder ~10 ns Распределённые ячейки

False Sharing

// Проблема: массив Atomic элементов в одной кэш-линии
AtomicInteger[] counters = new AtomicInteger[16];
// counters[0] и counters[1] могут быть в одной кэш-линии
// Запись в counters[0] инвалидирует counters[1]

Решение — @Contended внутри LongAdder:

@jdk.internal.vm.annotation.Contended
volatile long value; // 128 байт padding вокруг

AtomicIntegerFieldUpdater — экономия памяти

Если у вас миллионы объектов и в каждом нужен атомарный счётчик:

// ПЛОХО: миллионы объектов AtomicInteger в памяти
class User {
    AtomicInteger loginAttempts = new AtomicInteger(0); // +24 байта на объект
}

// ХОРОШО: updater работает с обычным int полем
class User {
    volatile int loginAttempts = 0; // 4 байта

    private static final AtomicIntegerFieldUpdater<User> UPDATER =
        AtomicIntegerFieldUpdater.newUpdater(User.class, "loginAttempts");

    public void increment() {
        UPDATER.incrementAndGet(this);
    }
}

Диагностика

CAS Failures в профайлере

# Java Flight Recorder — смотрите contention events
java -XX:StartFlightRecording=filename=rec.jfr MyApp

Если много времени в incrementAndGet — высокая contention.

Bytecode анализ

javap -c -p AtomicInteger.class

Инструкции getAndAddInt — нативные, транслируются в lock cmpxchg.

Best Practices

  1. Atomic* для простых счётчиков и флагов при умеренной конкуренции
  2. LongAdder для высоконагруженных счётчиков (метрики, статистика)
  3. AtomicReference с updateAndGet для immutable updates
  4. AtomicStampedReference когда важна ABA проблема
  5. synchronized для сложных операций (несколько полей одновременно)
  6. @Contended для массивов атомарных счётчиков (требует JVM флаг)
  7. Избегайте AtomicLong как ID-генератора при 1000+ RPS — используйте range allocation

🎯 Шпаргалка для интервью

Обязательно знать:

  • Atomic-классы обеспечивают lock-free потокобезопасность через CAS + volatile
  • incrementAndGet = спин-цикл: read → modify → CAS retry until success
  • На x86 CAS = lock cmpxchg инструкция с префиксом lock (блокировка шины памяти)
  • Java 9+ переписан на VarHandle (безопасная замена sun.misc.Unsafe, JEP 193)
  • ABA проблема: значение изменилось A→B→A, CAS «не заметит»; решение — AtomicStampedReference
  • LongAdder решает contention при 100+ потоках: распределяет по Cell[] ячейкам, суммирует при sum()
  • lazySet() — ослабленная видимость (eventual), быстрее чем set() — не ставит StoreLoad barrier

Частые уточняющие вопросы:

  • Почему AtomicLong тормозит при высокой конкуренции? — CAS постоянно фейлится, потоки крутят спин-цикл, тратя CPU
  • Чем LongAdder отличается от AtomicLong? — LongAdder: массив ячеек, каждый поток пишет в свою; AtomicLong: одна переменная, все конкурируют
  • Что такое ABA проблема? — Поток 1 прочитал A, Поток 2 изменил A→B→A, Поток 1: CAS(A) — успех, но состояние менялось
  • Когда использовать AtomicReference? — Для атомарной замены immutable объектов (например, обновление конфигурации)

Красные флаги (НЕ говорить):

  • “Atomic-классы используют блокировки” — нет, это lock-free через CAS
  • “CAS атомарен на любой архитектуре” — на 32-битных JVM AtomicLong может использовать внутреннюю блокировку
  • “lazySet() гарантирует немедленную видимость” — нет, eventual consistency, без StoreLoad барьера
  • “LongAdder всегда лучше AtomicLong” — нет, LongAdder быстрее только при высокой конкуренции; для обычных случаев AtomicLong

Связанные темы:

  • [[9. Что такое CAS (Compare-And-Swap)]] — основа работы всех Atomic-классов
  • [[1. В чём разница между synchronized и volatile]] — volatile для видимости + CAS для атомарности
  • [[3. Что такое visibility problem]] — volatile поле внутри Atomic обеспечивает видимость
  • [[7. Что такое reentrant lock]] — AQS (основа ReentrantLock) тоже использует CAS