Вопрос 11 · Раздел 9

В чём преимущество Atomic классов перед synchronized?

Atomic классы и synchronized — два разных подхода к потокобезопасности:

Версии по языкам: English Russian Ukrainian

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

  1. Atomic для простых операций при низкой/средней конкуренции
  2. synchronized для составных операций или при высокой конкуренции
  3. LongAdder для счётчиков при экстремальной нагрузке
  4. Избегайте Atomic для долгих вычислений — CAS-цикл сожжёт CPU
  5. Мониторьте: нет BLOCKED в дампе, но система тормозит → ищите горячие Atomic
  6. 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 (взаимная блокировка)]]