В чём преимущество 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 (взаимная блокировка)]]