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

Как работают AtomicInteger, AtomicLong?

AtomicInteger и AtomicLong — это потокобезопасные обёртки над примитивами int и long.

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

Junior уровень

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

AtomicInteger и AtomicLong — это потокобезопасные обёртки над примитивами int и long.

Зачем: когда несколько потоков одновременно меняют обычную int-переменную, обновления теряются. AtomicInteger решает эту проблему, гарантируя атомарность каждой операции без блокировок. Они обеспечивают атомарные (неделимые) операции без блокировок через CAS. Атомарная = другие потоки не могут увидеть промежуточное состояние.

Создание и базовые операции

// Создание
AtomicInteger atomicInt = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0L);

// Инкремент/декремент
atomicInt.incrementAndGet();  // ++i (возвращает новое)
atomicInt.getAndIncrement();  // i++ (возвращает старое)
atomicInt.decrementAndGet();  // --i
atomicInt.getAndDecrement();  // i--

// Арифметика
atomicInt.addAndGet(10);      // i += 10
atomicInt.getAndAdd(10);      // возвращает старое, потом += 10

// Установка
atomicInt.set(100);           // Прямая установка
atomicInt.getAndSet(200);     // Возвращает старое, устанавливает новое

// CAS
atomicInt.compareAndSet(200, 300); // Если == 200, установить 300

// Получение
int value = atomicInt.get();  // Текущее значение

Пример: Счётчик запросов

public class RequestCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void handleRequest(Request req) {
        int currentCount = count.incrementAndGet();
        System.out.println("Запрос #" + currentCount);
        // Обработка...
    }

    public int getTotalRequests() {
        return count.get();
    }
}

Почему не просто int?

// НЕ безопасно:
int count = 0;
count++; // 3 операции: read → modify → write
// Два потока могут прочитать одинаковое значение и потерять обновление

// Безопасно:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Атомарная операция — CAS на уровне CPU

Основные методы

Метод Описание Возвращает
get() Текущее значение int/long
set(v) Установить значение void
incrementAndGet() ++i Новое значение
getAndIncrement() i++ Старое значение
addAndGet(delta) i += delta Новое значение
getAndAdd(delta) i += delta Старое значение
compareAndSet(exp, upd) CAS true/false
updateAndGet(fn) Обновить через функцию Новое значение

Middle уровень

Внутренняя структура

public class AtomicInteger extends Number implements java.io.Serializable {
    // 1. volatile поле для видимости
    private volatile int value;

    // 2. VarHandle для прямого доступа к памяти (Java 9+)
    private static final VarHandle VALUE;

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

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
}

Различия в методах записи

AtomicInteger counter = new AtomicInteger(0);

// 1. set(v) — обычная volatile запись
counter.set(100);
// Сразу видно всем потокам, полный memory barrier

// 2. lazySet(v) — ослабленная запись (Java 9+: setOpaque)
counter.lazySet(100);
// Не гарантирует немедленную видимость, но быстрее, потому что не ставит полный memory barrier — процессор не обязан немедленно синхронизировать кэш-линии с другими ядрами.
// Полезно для сброса состояния, которое скоро будет прочитано тем же потоком

// 3. compareAndSet(exp, upd) — полноценный CAS
counter.compareAndSet(0, 100);
// Если == 0, установить 100. Полный barrier.

// 4. updateAndGet(fn) — обновление через функцию
counter.updateAndGet(x -> x * 2); // Умножить на 2
Метод Happens-Before Скорость Когда использовать
set(v) Да Стандарт Обычная запись
lazySet(v) Нет (eventual) Быстрее Сброс флагов
compareAndSet Да Стандарт Lock-free алгоритмы
weakCompareAndSet Нет Быстрее Оптимизированные циклы

Безопасность на 32-битных системах

// На 32-битной JVM:
long value = 0x0123456789ABCDEFL;
// Записывается в ДВА этапа:
// 1. 0x89ABCDEF (нижние 32 бита)
// 2. 0x01234567 (верхние 32 бита)
// Другой поток может увидеть "смешанное" значение!

// AtomicLong гарантирует атомарность:
AtomicLong atomicLong = new AtomicLong(0x0123456789ABCDEFL);
// Использует CMPXCHG8B инструкцию или внутреннюю блокировку

updateAndGet — гибкая атомарная операция

AtomicInteger flags = new AtomicInteger(0);

// Атомарно установить бит
flags.updateAndGet(current -> current | 0x01);

// Атомарно сбросить бит
flags.updateAndGet(current -> current & ~0x01);

// Атомарно переключить бит
flags.updateAndGet(current -> current ^ 0x01);

False Sharing (проблема кэш-линий)

Когда два AtomicInteger находятся рядом в массиве, они могут оказаться в одной 64-байтной кэш-линии CPU. Если поток A пишет в counters[0], а поток B читает counters[1] — они конкурируют за одну кэш-линию, даже если это разные счётчики.

Решение: @Contended аннотация (Java 8+, требует -XX:-RestrictContended) — добавляет padding вокруг поля, чтобы оно заняло отдельную кэш-линию.

@sun.misc.Contended
AtomicInteger counter; // 64 байта padding до и после

Для long[] массивов false sharing реален:

long[] counters = new long[8];
// counters[0] и counters[1] в одной кэш-линии → два потока конкурируют

Когда AtomicInteger — плохой выбор

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

Senior уровень

Under the Hood: VarHandle и Memory Access API

Java 9+ использует VarHandle вместо Unsafe:

// Atomic через VarHandle
public final int getAndAdd(int delta) {
    return (int)VALUE.getAndAdd(this, delta);
}

// VarHandle транслируется в нативные инструкции:
// VALUE.getAndAdd → lock xadd [memory], delta (x86)

getAndAdd на уровне x86

; lock xadd — атомарное сложение с возвратом старого значения
; eax = delta, [memory] = counter

lock xadd [memory], eax
; [memory] = [memory] + eax
; eax = старое значение [memory]

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

Для случаев когда миллионов объектов нужен атомарный int:

// ПЛОХО: миллионы дополнительных объектов AtomicInteger
class User {
    AtomicInteger loginAttempts = new AtomicInteger(0); // 24 байта × 1M = 24MB
}

// ХОРОШО: одно поле + один статический updater
class User {
    volatile int loginAttempts = 0; // 4 байта × 1M = 4MB

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

    public int incrementAttempts() {
        return UPDATER.incrementAndGet(this);
    }
}

CAS Contention и ID-генератор

// ПЛОХО: глобальный ID-генератор при 1000 RPS
AtomicLong idGenerator = new AtomicLong(0);
public long nextId() {
    return idGenerator.incrementAndGet(); // Высокая contention!
}

// ХОРОШО: ID range allocation
public class IdRangeGenerator {
    private final AtomicLong nextRangeStart = new AtomicLong(0);
    private static final long RANGE_SIZE = 1000;

    // Выдаёт пакет ID — каждый сервис/поток получает свой диапазон
    public IdRange allocateRange() {
        long start = nextRangeStart.getAndAdd(RANGE_SIZE);
        return new IdRange(start, start + RANGE_SIZE);
    }
}

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

Бенчмарк (примерный)

Операция Время Примечание
get() ~1 ns Volatile чтение
set() ~5 ns Volatile запись
incrementAndGet() (нет contention) ~5-10 ns Один CAS
incrementAndGet() (100 потоков) ~100+ ns CAS contention
lazySet() ~2-3 ns Без полного barrier

Оптимизация через batching

// Локальный счётчик + атомарный flush
public class BatchedCounter {
    private int local = 0;
    private final AtomicInteger flushed = new AtomicInteger(0);

    public synchronized void increment() {
        local++;
        if (local >= 100) { // Flush каждые 100
            flushed.addAndGet(local);
            local = 0;
        }
    }

    public int getTotal() {
        synchronized(this) {
            return flushed.get() + local;
        }
    }
}

Диагностика

Bytecode анализ

javap -c -p AtomicInteger.class
public final int getAndAddInt(int);
  Code:
    0: aload_0
    1: invokevirtual get
    4: iload_1
    5: iadd
    6: dup
    7: invokevirtual compareAndSet  // ← CAS вызов

Profiling CAS failures

// Отслеживаем количество CAS попыток vs успехов
AtomicLong successes = new AtomicLong(0);
AtomicLong attempts = new AtomicLong(0);

for (int i = 0; i < 1_000_000; i++) {
    attempts.incrementAndGet();
    if (counter.compareAndSet(expected, next)) {
        successes.incrementAndGet();
    }
}

double successRate = (double) successes.get() / attempts.get();
System.out.println("CAS success rate: " + (successRate * 100) + "%");
// < 50% → высокая contention

Best Practices

  1. AtomicInteger для счётчиков — при умеренной конкуренции
  2. AtomicLong для 64-битных счётчиков — безопасен на 32-битных JVM
  3. LongAdder при высокой конкуренции — распределённые ячейки
  4. lazySet для сброса флагов — быстрее чем set
  5. updateAndGet для сложных обновлений — атомарная функция
  6. AtomicIntegerFieldUpdater для экономии памяти — миллионы объектов
  7. ID range allocation — вместо глобального AtomicLong при 1000+ RPS

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

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

  • AtomicInteger/AtomicLong — потокобезопасные обёртки над int/long, обеспечивают атомарность без блокировок
  • Работают через CAS (compare-and-swap) — аппаратная инструкция CPU (lock cmpxchg на x86)
  • Java 9+ используют VarHandle вместо Unsafe для доступа к памяти
  • incrementAndGet() = спин-цикл: read → compute → CAS → retry при failure
  • lazySet() — ослабленная запись без полного memory barrier, быстрее для сброса флагов
  • При высокой конкуренции (100+ потоков) CAS contention убивает производительность — нужен LongAdder
  • AtomicLong гарантирует атомарность чтения/записи даже на 32-битных JVM (где long пишется в 2 этапа)
  • AtomicIntegerFieldUpdater экономит память когда миллионы объектов нуждаются в атомарном поле

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

  • Чем getAndIncrement() отличается от incrementAndGet()? — Первый возвращает старое значение (i++), второй — новое (++i)
  • Почему CAS лучше synchronized? — Нет context switch, deadlock невозможен, ~5ns против ~1000ns при низкой конкуренции
  • Что такое false sharing? — Два AtomicInteger в одной кэш-линии CPU конкурируют даже при разных адресах; решается @Contended
  • Когда AtomicInteger — плохой выбор? — Высокая конкуренция (LongAdder лучше), составные операции (нужен synchronized), долгие вычисления между read/write

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

  • “Atomic использует synchronized внутри” — нет, это lock-free CAS
  • “AtomicInteger всегда быстрее synchronized” — нет, при 8+ потоках synchronized может быть быстрее
  • “compareAndSet блокирует поток при неудаче” — нет, просто возвращает false, поток крутится в цикле

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

  • [[11. В чём преимущество Atomic классов перед synchronized]]
  • [[12. Что такое пул потоков (Thread Pool)]]
  • [[15. Что делает ExecutorService]]
  • [[18. Что такое deadlock (взаимная блокировка)]]