Как работают AtomicInteger, AtomicLong?
AtomicInteger и AtomicLong — это потокобезопасные обёртки над примитивами int и long.
Junior уровень
Базовое понимание
AtomicInteger и AtomicLong — это потокобезопасные обёртки над примитивами int и long.
Зачем: когда несколько потоков одновременно меняют обычную int-переменную, обновления теряются. AtomicInteger решает эту проблему, гарантируя атомарность каждой операции без блокировок. Они обеспечивают атомарные (неделимые) операции без блокировок через CAS. Атомарная = другие потоки не могут увидеть промежуточное состояние.
Создание и базовые операции
// Создание
AtomicInteger atomicInt = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0L);
// Инкремент/декремент
atomicInt.incrementAndGet(); // ++i (возвращает новое)
atomicInt.getAndIncrement(); // i++ (возвращает старое)
atomicInt.decrementAndGet(); // --i
atomicInt.getAndDecrement(); // i--
// Арифметика
atomicInt.addAndGet(10); // i += 10
atomicInt.getAndAdd(10); // возвращает старое, потом += 10
// Установка
atomicInt.set(100); // Прямая установка
atomicInt.getAndSet(200); // Возвращает старое, устанавливает новое
// CAS
atomicInt.compareAndSet(200, 300); // Если == 200, установить 300
// Получение
int value = atomicInt.get(); // Текущее значение
Пример: Счётчик запросов
public class RequestCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void handleRequest(Request req) {
int currentCount = count.incrementAndGet();
System.out.println("Запрос #" + currentCount);
// Обработка...
}
public int getTotalRequests() {
return count.get();
}
}
Почему не просто int?
// НЕ безопасно:
int count = 0;
count++; // 3 операции: read → modify → write
// Два потока могут прочитать одинаковое значение и потерять обновление
// Безопасно:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Атомарная операция — CAS на уровне CPU
Основные методы
| Метод | Описание | Возвращает |
|---|---|---|
get() |
Текущее значение | int/long |
set(v) |
Установить значение | void |
incrementAndGet() |
++i | Новое значение |
getAndIncrement() |
i++ | Старое значение |
addAndGet(delta) |
i += delta | Новое значение |
getAndAdd(delta) |
i += delta | Старое значение |
compareAndSet(exp, upd) |
CAS | true/false |
updateAndGet(fn) |
Обновить через функцию | Новое значение |
Middle уровень
Внутренняя структура
public class AtomicInteger extends Number implements java.io.Serializable {
// 1. volatile поле для видимости
private volatile int value;
// 2. VarHandle для прямого доступа к памяти (Java 9+)
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
public AtomicInteger(int initialValue) {
value = initialValue;
}
}
Различия в методах записи
AtomicInteger counter = new AtomicInteger(0);
// 1. set(v) — обычная volatile запись
counter.set(100);
// Сразу видно всем потокам, полный memory barrier
// 2. lazySet(v) — ослабленная запись (Java 9+: setOpaque)
counter.lazySet(100);
// Не гарантирует немедленную видимость, но быстрее, потому что не ставит полный memory barrier — процессор не обязан немедленно синхронизировать кэш-линии с другими ядрами.
// Полезно для сброса состояния, которое скоро будет прочитано тем же потоком
// 3. compareAndSet(exp, upd) — полноценный CAS
counter.compareAndSet(0, 100);
// Если == 0, установить 100. Полный barrier.
// 4. updateAndGet(fn) — обновление через функцию
counter.updateAndGet(x -> x * 2); // Умножить на 2
| Метод | Happens-Before | Скорость | Когда использовать |
|---|---|---|---|
set(v) |
Да | Стандарт | Обычная запись |
lazySet(v) |
Нет (eventual) | Быстрее | Сброс флагов |
compareAndSet |
Да | Стандарт | Lock-free алгоритмы |
weakCompareAndSet |
Нет | Быстрее | Оптимизированные циклы |
Безопасность на 32-битных системах
// На 32-битной JVM:
long value = 0x0123456789ABCDEFL;
// Записывается в ДВА этапа:
// 1. 0x89ABCDEF (нижние 32 бита)
// 2. 0x01234567 (верхние 32 бита)
// Другой поток может увидеть "смешанное" значение!
// AtomicLong гарантирует атомарность:
AtomicLong atomicLong = new AtomicLong(0x0123456789ABCDEFL);
// Использует CMPXCHG8B инструкцию или внутреннюю блокировку
updateAndGet — гибкая атомарная операция
AtomicInteger flags = new AtomicInteger(0);
// Атомарно установить бит
flags.updateAndGet(current -> current | 0x01);
// Атомарно сбросить бит
flags.updateAndGet(current -> current & ~0x01);
// Атомарно переключить бит
flags.updateAndGet(current -> current ^ 0x01);
False Sharing (проблема кэш-линий)
Когда два AtomicInteger находятся рядом в массиве, они могут оказаться в одной 64-байтной кэш-линии CPU. Если поток A пишет в counters[0], а поток B читает counters[1] — они конкурируют за одну кэш-линию, даже если это разные счётчики.
Решение: @Contended аннотация (Java 8+, требует -XX:-RestrictContended) — добавляет padding вокруг поля, чтобы оно заняло отдельную кэш-линию.
@sun.misc.Contended
AtomicInteger counter; // 64 байта padding до и после
Для long[] массивов false sharing реален:
long[] counters = new long[8];
// counters[0] и counters[1] в одной кэш-линии → два потока конкурируют
Когда AtomicInteger — плохой выбор
- Высокая конкуренция (100+ потоков): CAS contention убьёт производительность — используйте LongAdder
- Составные операции: обновление двух полей атомарно — нужен synchronized
- Долгие вычисления между read и write: CAS будет постоянно fail-ить — лучше Lock
Senior уровень
Under the Hood: VarHandle и Memory Access API
Java 9+ использует VarHandle вместо Unsafe:
// Atomic через VarHandle
public final int getAndAdd(int delta) {
return (int)VALUE.getAndAdd(this, delta);
}
// VarHandle транслируется в нативные инструкции:
// VALUE.getAndAdd → lock xadd [memory], delta (x86)
getAndAdd на уровне x86
; lock xadd — атомарное сложение с возвратом старого значения
; eax = delta, [memory] = counter
lock xadd [memory], eax
; [memory] = [memory] + eax
; eax = старое значение [memory]
AtomicIntegerFieldUpdater — экономия памяти
Для случаев когда миллионов объектов нужен атомарный int:
// ПЛОХО: миллионы дополнительных объектов AtomicInteger
class User {
AtomicInteger loginAttempts = new AtomicInteger(0); // 24 байта × 1M = 24MB
}
// ХОРОШО: одно поле + один статический updater
class User {
volatile int loginAttempts = 0; // 4 байта × 1M = 4MB
private static final AtomicIntegerFieldUpdater<User> UPDATER =
AtomicIntegerFieldUpdater.newUpdater(User.class, "loginAttempts");
public int incrementAttempts() {
return UPDATER.incrementAndGet(this);
}
}
CAS Contention и ID-генератор
// ПЛОХО: глобальный ID-генератор при 1000 RPS
AtomicLong idGenerator = new AtomicLong(0);
public long nextId() {
return idGenerator.incrementAndGet(); // Высокая contention!
}
// ХОРОШО: ID range allocation
public class IdRangeGenerator {
private final AtomicLong nextRangeStart = new AtomicLong(0);
private static final long RANGE_SIZE = 1000;
// Выдаёт пакет ID — каждый сервис/поток получает свой диапазон
public IdRange allocateRange() {
long start = nextRangeStart.getAndAdd(RANGE_SIZE);
return new IdRange(start, start + RANGE_SIZE);
}
}
Производительность и Highload
Бенчмарк (примерный)
| Операция | Время | Примечание |
|---|---|---|
get() |
~1 ns | Volatile чтение |
set() |
~5 ns | Volatile запись |
incrementAndGet() (нет contention) |
~5-10 ns | Один CAS |
incrementAndGet() (100 потоков) |
~100+ ns | CAS contention |
lazySet() |
~2-3 ns | Без полного barrier |
Оптимизация через batching
// Локальный счётчик + атомарный flush
public class BatchedCounter {
private int local = 0;
private final AtomicInteger flushed = new AtomicInteger(0);
public synchronized void increment() {
local++;
if (local >= 100) { // Flush каждые 100
flushed.addAndGet(local);
local = 0;
}
}
public int getTotal() {
synchronized(this) {
return flushed.get() + local;
}
}
}
Диагностика
Bytecode анализ
javap -c -p AtomicInteger.class
public final int getAndAddInt(int);
Code:
0: aload_0
1: invokevirtual get
4: iload_1
5: iadd
6: dup
7: invokevirtual compareAndSet // ← CAS вызов
Profiling CAS failures
// Отслеживаем количество CAS попыток vs успехов
AtomicLong successes = new AtomicLong(0);
AtomicLong attempts = new AtomicLong(0);
for (int i = 0; i < 1_000_000; i++) {
attempts.incrementAndGet();
if (counter.compareAndSet(expected, next)) {
successes.incrementAndGet();
}
}
double successRate = (double) successes.get() / attempts.get();
System.out.println("CAS success rate: " + (successRate * 100) + "%");
// < 50% → высокая contention
Best Practices
- AtomicInteger для счётчиков — при умеренной конкуренции
- AtomicLong для 64-битных счётчиков — безопасен на 32-битных JVM
- LongAdder при высокой конкуренции — распределённые ячейки
- lazySet для сброса флагов — быстрее чем set
- updateAndGet для сложных обновлений — атомарная функция
- AtomicIntegerFieldUpdater для экономии памяти — миллионы объектов
- ID range allocation — вместо глобального AtomicLong при 1000+ RPS
🎯 Шпаргалка для интервью
Обязательно знать:
- AtomicInteger/AtomicLong — потокобезопасные обёртки над int/long, обеспечивают атомарность без блокировок
- Работают через CAS (compare-and-swap) — аппаратная инструкция CPU (lock cmpxchg на x86)
- Java 9+ используют VarHandle вместо Unsafe для доступа к памяти
- incrementAndGet() = спин-цикл: read → compute → CAS → retry при failure
- lazySet() — ослабленная запись без полного memory barrier, быстрее для сброса флагов
- При высокой конкуренции (100+ потоков) CAS contention убивает производительность — нужен LongAdder
- AtomicLong гарантирует атомарность чтения/записи даже на 32-битных JVM (где long пишется в 2 этапа)
- AtomicIntegerFieldUpdater экономит память когда миллионы объектов нуждаются в атомарном поле
Частые уточняющие вопросы:
- Чем getAndIncrement() отличается от incrementAndGet()? — Первый возвращает старое значение (i++), второй — новое (++i)
- Почему CAS лучше synchronized? — Нет context switch, deadlock невозможен, ~5ns против ~1000ns при низкой конкуренции
- Что такое false sharing? — Два AtomicInteger в одной кэш-линии CPU конкурируют даже при разных адресах; решается @Contended
- Когда AtomicInteger — плохой выбор? — Высокая конкуренция (LongAdder лучше), составные операции (нужен synchronized), долгие вычисления между read/write
Красные флаги (НЕ говорить):
- “Atomic использует synchronized внутри” — нет, это lock-free CAS
- “AtomicInteger всегда быстрее synchronized” — нет, при 8+ потоках synchronized может быть быстрее
- “compareAndSet блокирует поток при неудаче” — нет, просто возвращает false, поток крутится в цикле
Связанные темы:
- [[11. В чём преимущество Atomic классов перед synchronized]]
- [[12. Что такое пул потоков (Thread Pool)]]
- [[15. Что делает ExecutorService]]
- [[18. Что такое deadlock (взаимная блокировка)]]