Что такое CAS (Compare-And-Swap)?
Представьте банкомат: 4. Если да — снимает 200, новый баланс 800 5. Если нет (кто-то уже снял) — отменяет операцию, нужно попробовать снова
Junior уровень
Базовое понимание
CAS (Compare-And-Swap) — это атомарная инструкция процессора, которая позволяет обновить значение только если оно не изменилось с момента чтения. Это основа lock-free алгоритмов.
Простая аналогия
Представьте банкомат:
- Вы смотрите баланс: 1000 руб
- Вы хотите снять 200 руб (ожидаете что будет 800)
- Банкомат проверяет: “баланс всё ещё 1000?”
- Если да — снимает 200, новый баланс 800
- Если нет (кто-то уже снял) — отменяет операцию, нужно попробовать снова
Ключевое отличие от synchronized: CAS — это optimistic подход (попробуй, если не вышло — повтори). synchronized — pessimistic подход (заблокируй и делай, никто не помешает).
Как работает CAS
// CAS принимает 3 параметра:
// 1. Адрес в памяти
// 2. Ожидаемое значение
// 3. Новое значение
boolean CAS(address, expectedValue, newValue) {
if (memory[address] == expectedValue) {
memory[address] = newValue;
return true; // Успех!
}
return false; // Не удалось — значение изменилось
}
Пример с AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // Внутри:
// 1. Прочитать текущее значение (current = 0)
// 2. Вычислить новое (next = 1)
// 3. CAS: если value == 0, установить 1
// Если CAS не удался — повторить с шага 1
Спин-цикл (Spin-loop)
public final int incrementAndGet() {
for (;;) { // Бесконечный цикл
int current = get(); // Прочитать
int next = current + 1; // Вычислить
if (compareAndSet(current, next)) { // CAS
return next; // Успех!
}
// CAS не удался — кто-то другой обновил значение
// Идём на следующий круг цикла
}
}
Когда CAS быстрее блокировок?
| Сценарий | CAS | synchronized |
|---|---|---|
| Низкая конкуренция | Быстрее (нет блокировки) | Медленнее (захват/освобождение) |
| Средняя конкуренция | Быстрее | Средне |
| Высокая конкуренция | Медленнее (бесконечные циклы) | Быстрее (потоки парковались) |
Middle уровень
Алгоритм на уровне железа
На x86 архитектуре CAS реализуется через инструкцию LOCK CMPXCHG:
; rax = expected value
; rcx = new value
; [memory] = actual value in memory
lock cmpxchg [memory], rcx
; Если [memory] == rax:
; [memory] = rcx ← Записываем новое значение
; ZF = 1 ← Флаг успеха
; Иначе:
; rax = [memory] ← Загружаем актуальное значение
; ZF = 0 ← Флаг неудачи
Префикс lock блокирует шину памяти на время операции — другие ядра не могут получить доступ к этой ячейке.
CAS в Java: Unsafe и VarHandle
До Java 9: sun.misc.Unsafe
До Java 9: sun.misc.Unsafe (прямой доступ к памяти, опасен)
Java 9+: VarHandle (безопасная замена Unsafe, JEP 193)
Примечание: Unsafe всё ещё доступен в Java 9+ через --add-opens, но не рекомендуется.
// Прямой доступ к памяти (не рекомендуется)
Unsafe unsafe = ...;
long offset = ...; // Смещение поля в памяти
int current;
do {
current = unsafe.getIntVolatile(obj, offset);
} while (!unsafe.compareAndSwapInt(obj, offset, current, current + 1));
Java 9+: VarHandle
// Типизированный и безопасный API
private static final VarHandle VALUE;
static {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(MyClass.class, "value", int.class);
}
int current;
do {
current = (int) VALUE.getVolatile(this);
} while (!VALUE.compareAndSet(this, current, current + 1));
Lock-free vs Wait-free
На собеседовании важно различать:
Это иерархия progress guarantee (гарантий прогресса):
- Wait-free (сильнейшая): КАЖДЫЙ поток завершит операцию за конечное число шагов
- Lock-free (средняя): ХОТЯ БЫ ОДИН поток завершит операцию за конечное число шагов
- Obstruction-free (слабейшая): поток завершит, если все остальные приостановлены
Wait-free → Lock-free → Obstruction-free: каждая следующая слабее предыдущей.
| Термин | Гарантия | Пример |
|---|---|---|
| Lock-free | Хотя бы один поток завершит операцию за конечное время | AtomicInteger |
| Wait-free | Каждый поток завершит за конечное число шагов | LongAdder.add() |
| Obstruction-free | Поток завершится, если другие не мешают | Некоторые STM реализации |
STM (Software Transactional Memory) — программная транзакционная память: операции выполняются в транзакции, потом атомарно коммитятся. Альтернативный подход к lock-free программированию.
Exponential Back-off
При высокой конкуренции простой спин-цикл тратит CPU. Решение — паузы:
public int incrementWithBackoff() {
int backoff = 1;
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
// CAS не удался — делаем паузу
Thread.onSpinWait(); // Подсказка процессору
// `Thread.onSpinWait()` — hint JVM процессору: «я в spin-цикле, можешь оптимизировать». На x86 это PAUSE-инструкция, которая снижает энергопотребление и улучшает производительность spin-цикла.
Thread.sleep(ThreadLocalRandom.current().nextInt(0, backoff));
backoff = Math.min(backoff * 2, MAX_BACKOFF); // Экспоненциальный рост
}
}
ABA Проблема
Время Поток 1 Поток 2 Стек (head)
──────────────────────────────────────────────────
t1 читает A A → B → C
t2 CAS(A, B) B → C
t3 CAS(B, A) A → C (новое значение!)
t4 CAS(A, D) → УСПЕХ!
Но стек изменился! B и C потеряны!
Решение: AtomicStampedReference
// Хранит значение + номер версии
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String value = ref.get(stampHolder);
int stamp = stampHolder[0];
// CAS только если и значение И версия совпадают
ref.compareAndSet("A", "D", stamp, stamp + 1);
Когда CAS НЕ подходит
- Составные операции: нужно атомарно обновить два поля — CAS работает с одним полем
- High contention (100+ потоков): CAS-цикл тратит CPU впустую, лучше LongAdder или Lock
- Долгие вычисления: если между read и CAS проходят миллисекунды — CAS будет постоянно fail-ить
- ABA проблема: значение изменилось A→B→A, CAS «не заметит» — нужен AtomicStampedReference
Senior уровень
Under the Hood: Memory Ordering
CAS обеспечивает полный memory barrier (StoreLoad):
До CAS: Все записи гарантированно завершены
CAS: Атомарное обновление
После CAS: Все чтения видят актуальные данные
На разных архитектурах:
| Архитектура | CAS реализация | Barriers |
|---|---|---|
| x86 | lock cmpxchg |
Full barrier (x86 сильно упорядочен) |
| ARM | ldxr/stxr loop |
Требует dmb инструкции |
| RISC-V | lr.w/sc.w loop |
Требует fence |
Спин-цикл и CPU Saturation
При очень высокой конкуренции потоки могут “крутиться” вечно:
// Проблема: 100 потоков, 1 CAS-переменная
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) // Все 100 потоков постоянно фейлятся
return next;
// CPU на 100%, но прогресса нет!
}
Решения:
- Exponential Back-off — пауза между попытками
- LongAdder — распределение по ячейкам
- Переход к блокировке — если contention слишком высокий
Производительность: CAS vs Mutex
| Метрика | CAS | Mutex (synchronized) |
|---|---|---|
| Latency (uncontended) | ~5-10 ns | ~10-20 ns |
| Latency (contended) | ~100+ ns (spin) | ~1000+ ns (park) |
| CPU usage (contended) | 100% (spin-loop) | ~0% (parked) |
| Throughput (low contention) | Выше | Ниже |
| Throughput (high contention) | Ниже | Выше |
Критический момент: При сверхвысокой нагрузке мьютекс может быть выгоднее, так как он “паркует” потоки, а не заставляет сжигать CPU.
Реализация AtomicLong на 32-битных системах
// На 32-битной JVM запись long не атомарна!
// long = 64 бита, записывается в два этапа по 32 бита
// AtomicLong гарантирует атомарность через:
// 1. Инструкцию CMPXCHG8B (x86)
// 2. Или внутреннюю блокировку (на старых CPU)
Bus Traffic и Cache Coherence
На многопроцессорных системах частые CAS вызывают:
- Cache Line Invalidation — каждое CAS посылает Invalidate всем ядрам
- Bus Traffic — интенсивный обмен по шине межпроцессорной связи
- Memory Bandwidth Saturation — шина забита, обычные операции тормозят
Решение: Сегментированные структуры (как ConcurrentHashMap) или LongAdder.
Диагностика
JMC (Java Mission Control)
java -XX:StartFlightRecording=filename=rec.jfr MyApp
События:
jdk.ExecutionSample— показывает, где потоки тратят время- Если много в
compareAndSet→ contention
-XX:+PrintAssembly
java -XX:+PrintAssembly -XX:+UnlockDiagnosticVMOptions
Ищите:
lock cmpxchg dword ptr [rax], rcx # ← Это CAS
Определение contention
// Программная оценка contention
AtomicLong counter = new AtomicLong(0);
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
counter.incrementAndGet();
}
long elapsed = System.nanoTime() - start;
System.out.println("Среднее время: " + (elapsed / 1_000_000) + " ns");
// Много > 10 ns → contention
Best Practices
- CAS для простых операций — increment, set, get
- Back-off при contention —
Thread.onSpinWait()для подсказки CPU - LongAdder для счётчиков — при высокой конкуренции
- AtomicStampedReference — когда возможна ABA проблема
- Не используйте CAS для долгих операций — переходите к блокировкам
- Мониторьте CAS failures — через JFR или профилирование
- Избегайте CAS для нескольких полей — используйте synchronized
🎯 Шпаргалка для интервью
Обязательно знать:
- CAS = Compare-And-Swap: атомарно сравнивает значение с ожидаемым и записывает новое, если совпало
- На x86:
lock cmpxchg(префиксlockблокирует шину памяти); на ARM:ldxr/stxr; на RISC-V:lr.w/sc.w - CAS обеспечивает полный memory barrier (StoreLoad) — все записи до CAS завершены, все чтения после CAS актуальны
- CAS — это optimistic подход (попробуй/повтори), synchronized — pessimistic (заблокируй и делай)
- Lock-free (AtomicInteger) vs Wait-free (LongAdder): lock-free = хотя бы один поток завершит; wait-free = каждый поток завершит
- ABA проблема: значение вернулось A→B→A, CAS не заметит; решение — AtomicStampedReference (значение + версия)
- При высокой конкуренции CAS-цикл тратит CPU (100%); решение — exponential back-off или переход к LongAdder/Lock
Частые уточняющие вопросы:
- Когда CAS быстрее synchronized? — При низкой/средней конкуренции (нет overhead на блокировку/парковку)
- Когда synchronized быстрее CAS? — При высокой конкуренции (100+ потоков): CAS крутится и жжёт CPU, mutex паркует потоки
- Что делает
Thread.onSpinWait()? — Hint JVM процессору «я в spin-цикле»; на x86 = PAUSE инструкция, снижает энергопотребление - Чем CAS отличается от volatile? — volatile = только видимость; CAS = видимость + атомарная запись (compare-and-swap)
Красные флаги (НЕ говорить):
- “CAS всегда быстрее synchronized” — нет, при высокой конкуренции mutex эффективнее
- “CAS не имеет проблем с памятью” — нет, CAS ставит полный memory barrier, что дорого
- “ABA проблема — это баг Java” — нет, это фундаментальная проблема CAS-алгоритмов на любом языке
- “Wait-free и lock-free — одно и то же” — нет, wait-free сильнее: КАЖДЫЙ поток завершит, lock-free — хотя бы ОДИН
Связанные темы:
- [[8. Что такое Atomic классы]] — все Atomic-классы построены на CAS
- [[1. В чём разница между synchronized и volatile]] — CAS vs synchronized подходы
- [[3. Что такое visibility problem]] — CAS обеспечивает полный memory barrier
- [[7. Что такое reentrant lock]] — AQS использует CAS для захвата state