Що таке Atomic класи?
Звичайна операція count++ не потокобезпечна:
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 класи працюють на двох стовпах:
volatileполе — гарантує видимість змін- 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 — це інструкція процесора, яка атомарно:
- Порівнює значення за адресою з очікуваним
- Якщо дорівнює — записує нове
- Якщо не дорівнює — нічого не робить
// Псевдокод 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 класи НЕ підходять
- Складні операції над кількома полями: потрібно атомарно оновити два поля — AtomicInteger не допоможе, потрібен synchronized або AtomicReference з immutable-об’єктом
- Висока конкуренція (100+ потоків): CAS-цикл марнує CPU — краще LongAdder
- Довгі обчислення між read та write: якщо між читанням та записом проходить багато часу — CAS буде постійно фейлитися, краще 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
- Atomic* для простих лічильників та прапорців при помірній конкуренції
- LongAdder для високонавантажених лічильників (метрики, статистика)
- AtomicReference з
updateAndGetдля immutable updates - AtomicStampedReference коли важлива ABA проблема
- synchronized для складних операцій (кілька полів одночасно)
- @Contended для масивів атомарних лічильників (потребує JVM прапорця)
- Уникайте 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