Что такое 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 будет постоянно fail-ить, лучше 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