Питання 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 буде постійно фейлитися — краще 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 (взаємне блокування)]]