В чому перевага Atomic класів перед synchronized?
Atomic класи та synchronized — два різних підходи до потокобезпечності:
Junior рівень
Базове розуміння
Atomic класи та synchronized — два різних підходи до потокобезпечності:
| Підхід | Принцип | Аналогія |
|---|---|---|
| synchronized | Песимістичний: “Конкуренція буде — блокую одразу” | Одна людина входить в кімнату, інші чекають в черзі |
| Atomic | Оптимістичний: “Конкуренція малоймовірна — спробую швидко” | Всі намагаються зайти одночасно, хто встиг — той оновив |
Простий приклад
// synchronized — песиміст
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++; // Тільки один потік одночасно, інші чекають
}
}
// Atomic — оптиміст
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Швидка CAS спроба, якщо не вийшло — ще раз
}
}
Коли що використовувати
| Ситуація | Вибір | Чому |
|---|---|---|
| Простий лічильник | Atomic | Швидше (CAS ~5ns vs synchronized ~1000ns — немає context switch), немає deadlock |
| Складна операція | synchronized | Потрібно блокувати кілька полів |
| Прапорець | AtomicBoolean | Легше synchronized |
| Оновлення кількох полів | synchronized | Atomic тільки для одного поля |
Middle рівень
Чому Atomic швидший?
1. Відсутність Context Switch
synchronized (при конкуренції):
Потік → BLOCKED → ОС паркує → Context Switch → ~1000-5000 ns
Atomic (CAS):
Потік → RUNNABLE → CAS спроба → ~5-10 ns (без context switch)
При synchronized потік переходить у стан BLOCKED, що потребує звернення до ядра ОС та перемикання контексту — це тисячі тактів CPU.
Atomic використовує Spin-waiting (буквально «крутіння в очікуванні»): потік не засинає, а в циклі повторює CAS-спробу. Це швидше, тому що не передає керування операційній системі (немає context switch).
2. Progress Guarantees (Гарантії прогресу)
| Проблема | synchronized | Atomic |
|---|---|---|
| Deadlock | Можливий | Неможливий (для окремих Atomic-операцій; але бізнес-логіка на основі кількох Atomic все ще може зависнути) |
| Priority Inversion | Можливий | Неможливий |
| Забутий unlock | Н/Д (автоматично) | Н/Д |
// synchronized — можливий deadlock
synchronized(lockA) {
synchronized(lockB) { // Якщо інший потік зробив навпаки → deadlock
// ...
}
}
// Atomic — deadlock неможливий
counter1.incrementAndGet(); // Завжди завершиться
counter2.incrementAndGet(); // Завжди завершиться
Як працює CAS-цикл
// Спрощена реалізація incrementAndGet
public final int incrementAndGet() {
for (;;) { // Спін-цикл
int current = get(); // 1. Читаємо volatile значення
int next = current + 1; // 2. Обчислюємо нове
if (compareAndSet(current, next)) { // 3. CAS
return next; // Успіх!
}
// CAS не вдався — хтось інший оновив значення
// Йдемо на наступний круг (без блокування!)
}
}
Коли Atomic ГИРШИЙ, ніж synchronized?
1. Висока конкуренція (High Contention)
100 потоків одночасно роблять incrementAndGet():
- CAS постійно фейлиться
- Потоки крутяться в циклах, спалюючи CPU на 100%
- Прогресу майже немає
В цьому випадку synchronized може бути ефективнішим:
- Потоки вишиковуються в чергу
- CPU не витрачається на порожні циклі
2. Складні операції
// Atomic НЕ допоможе атомарно оновити x та y
AtomicInteger x = new AtomicInteger(0);
AtomicInteger y = new AtomicInteger(0);
// НЕ безпечно!
void swap() {
int temp = x.get();
x.set(y.get()); // Інший потік може побачити проміжний стан
y.set(temp);
}
// synchronized — безпечно
synchronized(lock) {
int temp = x;
x = y;
y = temp;
}
3. Тривалі операції
// ПОГАНО: довга операція в CAS-циклі
atomicRef.updateAndGet(current -> {
// Довгі обчислення — CPU згорить при конкуренції!
return expensiveTransformation(current);
});
// ГАЗАРД: довга операція в synchronized
synchronized(lock) {
ref = expensiveTransformation(ref); // Інші потоки чекають, CPU не згорає
}
Senior рівень
Under the Hood: Bus Contention
Навіть коли CAS проходить успішно, він генерує трафік по шині даних:
Потік на Ядрі 1 робить CAS:
1. lock cmpxchg [memory], new_value
2. Блокування шини пам'яті
3. Invalidate кеш-ліній на всіх інших ядрах
4. Очікування Ack від всіх ядер
5. Розблокування шини
Багато Atomic-змінних можуть “забити” шину пам’яті сервера.
Lock-free vs Wait-free vs Obstruction-free
| Термін | Гарантія | Приклад |
|---|---|---|
| Lock-free | Хоча б один потік завершить за скінченний час | AtomicInteger |
| Wait-free | Кожен потік завершить за скінченну кількість кроків | LongAdder.add() |
| Obstruction-free | Потік завершиться, якщо інші не заважають | Деякі STM |
synchronized — жоден з перелічених (блокуючий):
- Потік може бути заблокований нескінченно (deadlock, starvation)
Performance: Точка перелому
К-сть потоків | Atomic (ns/op) | synchronized (ns/op)
───────────────────────────────────────────────────────
1 | 5 | 10
2 | 10 | 15
4 | 20 | 20
8 | 50 | 25
16 | 200 | 30
32 | 1000+ | 40
64 | Spin! | 50
Цифри приблизні, залежать від CPU/JVM. Вимірюйте на своєму залізі через JMH.
Точка перелому: ~4-8 потоків для простих операцій. Після цього synchronized стає швидшим.
LongAdder як розвиток Atomic ідеї
// Проблема: AtomicInteger при 100 потоках → CAS contention
AtomicInteger counter = new AtomicInteger(0);
// Рішення: LongAdder розподіляє по комірках
LongAdder counter = new LongAdder();
counter.increment(); // Кожен потік у свою комірку
long total = counter.sum(); // Сумує всі комірки
LongAdder внутрішньо:
// Базове значення + масив комірок
volatile long base;
volatile Cell[] cells; // Кожен потік хешується у свою комірку
// @Contended padding — уникнення False Sharing
static final class Cell {
@Contended
volatile long value;
}
Діагностика
Thread Dumps
jstack <pid>
# Якщо гальмує synchronized — бачимо BLOCKED потоки
"worker-1" BLOCKED (on object monitor)
- waiting to lock <0x000...>
# Якщо гальмує Atomic — BLOCKED немає, потоки в RUNNABLE
"worker-1" RUNNABLE
at AtomicInteger.compareAndSet(...)
// Потік крутиться в CAS-циклі
-XX:+PrintAssembly
# synchronized → важкі виклики ОС
call runtime_monitor_enter
# Atomic → одна інструкція
lock cmpxchg dword ptr [rax], rcx
Best Practices
- Atomic для простих операцій при низькій/середній конкуренції
- synchronized для складних операцій або при високій конкуренції
- LongAdder для лічильників при екстремальному навантаженні
- Уникайте Atomic для довгих обчислень — CAS-цикл спалить CPU
- Моніторьте: немає BLOCKED в дампі, але система гальмує → шукайте гарячі Atomic
- Back-off:
Thread.onSpinWait()для підказки процесору при CAS retry
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Atomic = оптимістичний підхід (спробуй швидко, retry при конфлікті), synchronized = песимістичний (одразу блокую)
- Atomic використовує CAS-цикл (spin-waiting): потік не засинає, а повторює спробу — немає context switch
- synchronized переводить потік у BLOCKED (~1000-5000 ns), Atomic залишається в RUNNABLE (~5-10 ns)
- Точка перелому: ~4-8 потоків для простих операцій — після цього synchronized може стати швидшим
- Atomic гарантує lock-free прогрес (хоча б один потік завершиться), synchronized може deadlock-нутись
- Atomic НЕ допомагає для складних операцій (оновлення 2+ полів) — тут synchronized необхідний
- При високій конкуренції CAS постійно фейлиться, потоки спалюють CPU на 100% — LongAdder або synchronized краще
- LongAdder розвиває ідею Atomic: розподіляє лічильник по комірках (per-thread), сумує при читанні
Часті уточнюючі запитання:
- Чому Atomic швидший при низькій конкуренції? — Немає context switch та звернення до ОС, одна інструкція lock cmpxchg
- Що таке lock-free vs wait-free? — Lock-free: хоча б один потік завершиться; wait-free: кожен потік завершить за скінченну кількість кроків (LongAdder.add)
- Що відбувається на рівні CPU при CAS? — Блокування шини пам’яті → invalidate кеш-ліній на всіх ядрах → очікування Ack → розблокування
- Коли synchronized кращий за Atomic? — Висока конкуренція (8+ потоків), складні операції, тривалі обчислення
Червоні прапори (НЕ говорити):
- “Atomic завжди швидший за synchronized” — ні, точка перелому ~4-8 потоків
- “Atomic використовує блокування” — ні, це lock-free CAS-цикл
- “З Atomic неможливий deadlock в будь-якому коді” — окремі Atomic-операції безпечні, але бізнес-логіка на кількох Atomic все ще може зависнути
Пов’язані теми:
- [[10. Як працюють AtomicInteger, AtomicLong]]
- [[12. Що таке пул потоків (Thread Pool)]]
- [[18. Що таке deadlock (взаємне блокування)]]