Как работает compare-and-swap операция в атомиках
Что проверяют
Понимаете ли вы принцип неблокирующей синхронизации и знаете ли проблему ABA.
Как отвечать
«Сравнить и обменять» это атомарная операция процессора: она проверяет, что в ячейке лежит ожидаемое значение, и только тогда записывает новое. Возвращает признак успеха. Всё это выполняется как одна неделимая инструкция, поэтому между проверкой и записью никто не вклинится.
На ней построены атомарные классы:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();Внутри это цикл: прочитали текущее значение, посчитали новое, попытались записать при условии, что текущее не изменилось. Не получилось, значит кто-то опередил, читаем заново и повторяем.
Ключевое отличие от блокировки: поток не засыпает. Нет захвата монитора, нет переключения контекста, нет постановки в очередь ожидания. Это называют неблокирующей синхронизацией.
Отсюда компромисс:
- при низкой и средней конкуренции атомики заметно быстрее блокировок;
- при высокой конкуренции цикл повторов начинает жечь процессор впустую: десятки потоков читают, считают и проигрывают гонку. Для счётчиков это решает
LongAdder, который разносит значение по нескольким ячейкам и складывает их только при чтении.
Про видимость: операция обладает семантикой изменчивого поля, то есть даёт и атомарность, и гарантию видимости изменений другим потокам.
Проблема ABA, о которой обычно и спрашивают следом: значение успело измениться с A на B и обратно на A. Операция сравнения этого не заметит и решит, что ничего не происходило. Для простых счётчиков это безразлично, а для структур со ссылками может привести к порче данных. Решается добавлением версии: в Java для этого есть AtomicStampedReference.
Полезно упомянуть, что в современном коде вместо старых атомарных классов используют VarHandle, дающий тот же набор операций с более гибким контролем упорядочивания.
Где спотыкаются
Считают атомики бесплатными при любой конкуренции.
Не знают про проблему ABA.
Пытаются собрать составную операцию из нескольких атомарных вызовов и получают гонку.
Используют обычный счётчик под нагрузкой вместо LongAdder.