Як працюють AtomicInteger, AtomicLong?
AtomicInteger та AtomicLong — це потокобезпечні обгортки над примітивами int та long.
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 — поганий вибір
- Висока конкуренція (100+ потоків): CAS contention вб’є продуктивність — використовуйте LongAdder
- Складні операції: оновлення двох полів атомарно — потрібен synchronized
- Довгі обчислення між 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
- AtomicInteger для лічильників — при помірній конкуренції
- AtomicLong для 64-бітних лічильників — безпечний на 32-бітних JVM
- LongAdder при високій конкуренції — розподілені комірки
- lazySet для скидання прапорців — швидше ніж set
- updateAndGet для складних оновлень — атомарна функція
- AtomicIntegerFieldUpdater для економії пам’яті — мільйони об’єктів
- 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 (взаємне блокування)]]