AtomicReference.compareAndSet: примеры (JAVA)

Руководство по AtomicReference.compareAndSet в Java
Раздел: Многопоточность, Атомарные классы
AtomicReference.compareAndSet(V expect, V update): boolean

Общее описание и сигнатура

Метод AtomicReference.compareAndSet() из пакета java.util.concurrent.atomic выполняет атомарную операцию сравнения и замены значения ссылки. Он используется в многопоточной среде для неблокирующих обновлений разделяемой ссылки без явной синхронизации.

Сигнатура:

public final boolean compareAndSet(V expect, V update)

Параметры:

  • expect - ожидаемая ссылка (тип V). Сравнение происходит по ссылке (по идентичности), то есть по сравнению текущего значения и expect как ссылок.
  • update - новое значение (тип V), которое будет записано при совпадении.

Возвращаемое значение:

  • true - если текущее значение ссылочной переменной совпало с expect и замена успешна.
  • false - если текущее значение не совпало с expect, операция не выполнила запись.

Память и видимость: успешный вызов имеет семантику записи volatile (изменение видимо другим потокам). Неуспешный вызов не выполняет запись и не обеспечивает дополнительных эффектов записи.

Поведение сравнения: сравнение происходит по ссылочной идентичности (эквивалентно проверке current == expect). При использовании объектных типов следует учитывать автопаковку и кэширование (например, Integer.valueOf), это влияет на сравнение по ссылке.

Когда используется: в сценариях без блокировок для реализации неблокирующих алгоритмов (спин-циклы обновления, неблокирующие коллекции, ленивые инициализации и т.д.). Часто применяется вместе с циклами, которые повторяют попытки до успеха.

Короткие примеры применения

Пример 1: простая успешная и неуспешная замена

import java.util.concurrent.atomic.AtomicReference;

public class Demo1 {
    public static void main(String[] args) {
        AtomicReference ref = new AtomicReference<>("A");
        boolean r1 = ref.compareAndSet("A", "B");
        boolean r2 = ref.compareAndSet("A", "C");
        System.out.println(r1 + ", value=" + ref.get());
        System.out.println(r2 + ", value=" + ref.get());
    }
}
true, value=B
false, value=B

Пример 2: ловушка с автопаковкой Integer (сравнение по ссылке)

import java.util.concurrent.atomic.AtomicReference;

public class Demo2 {
    public static void main(String[] args) {
        AtomicReference ref = new AtomicReference<>(Integer.valueOf(100));
        Integer expect = Integer.valueOf(100); // кэш -128..127, может быть та же ссылка
        boolean r = ref.compareAndSet(expect, Integer.valueOf(200));
        System.out.println(r + ", value=" + ref.get());

        AtomicReference ref2 = new AtomicReference<>(new Integer(100));
        boolean r2 = ref2.compareAndSet(new Integer(100), 200); // разные ссылки
        System.out.println(r2 + ", value=" + ref2.get());
    }
}
true, value=200
false, value=100

Пример 3: простая петля обновления (retry loop)

import java.util.concurrent.atomic.AtomicReference;

public class Demo3 {
    public static void main(String[] args) {
        AtomicReference ref = new AtomicReference<>("v1");
        while (true) {
            String cur = ref.get();
            String next = cur + "-updated";
            if (ref.compareAndSet(cur, next)) {
                System.out.println("Updated to " + ref.get());
                break;
            }
        }
    }
}
Updated to v1-updated

Похожие Java-методы и выбор между ними

  • weakCompareAndSet - похожая по имени операция, допускает ложные неудачи (спонтанные), но может быть быстрее на некоторых архитектурах; подходит для внутренней оптимизации в петлях, допускающих повторные попытки.
  • getAndSet - атомарно заменяет значение и возвращает предыдущее; удобен, когда нужно получить старое значение вне цикла.
  • AtomicInteger.compareAndSet и другие атомарные числовые типы - аналогичные методы для примитивных типов, работают по значению, не по ссылке.
  • AtomicStampedReference / AtomicMarkableReference - решают проблему ABA, позволяют хранить дополнительный маркер или отметку версии вместе со ссылкой.
  • VarHandle.compareAndSet - более новый, гибкий механизм (с явным указанием памяти режима) для работы с полями; полезен при низкоуровневой оптимизации.

Когда что предпочтительнее: для простых ссылочных обновлений достаточно AtomicReference.compareAndSet. Если возможна проблема ABA - рассмотреть AtomicStampedReference. Если требуется более низкоуровневый контроль над семантикой памяти, подходят VarHandle-версии.

Альтернативы в других языках и отличия

Краткие сведения и примеры.

  • C#: Interlocked.CompareExchange<T>(ref location, value, comparand). Сравнение по ссылке для ссылочных типов и по значению для примитивов. Возвращает прежнее значение.
    using System;
    using System.Threading;
    
    class CSharpDemo {
        static object location = "A";
        static void Main() {
            var prev = Interlocked.CompareExchange(ref location, "B", "A");
            Console.WriteLine(prev + ", current=" + location);
        }
    }
    A, current=B
  • C / C++: в C11/C++11 есть атомики: atomic_compare_exchange_strong возвращает флаг успеха и обновляет ожидаемое значение при неудаче.
    #include <atomic>
    #include <iostream>
    
    int main(){
        std::atomic a{1};
        int expected = 1;
        bool ok = a.compare_exchange_strong(expected, 2);
        std::cout << ok << ", value=" << a << std::endl;
    }
    
    1, value=2
  • Go: пакет sync/atomic предоставляет CompareAndSwapInt32/Int64/Pointer. Для произвольных объектов применяется atomic.Value и низкоуровневые операции с unsafe.Pointer.
    package main
    
    import (
        "fmt"
        "sync/atomic"
        "unsafe"
    )
    
    func main(){
        var p unsafe.Pointer
        s := "A"
        atomic.StorePointer(&p, unsafe.Pointer(&s))
        old := unsafe.Pointer(&s)
        t := "B"
        swapped := atomic.CompareAndSwapPointer(&p, old, unsafe.Pointer(&t))
        fmt.Println(swapped)
    }
    
    true
  • JavaScript: Web Workers + SharedArrayBuffer + Atomics не дают CAS для объектов, но есть Atomics.compareExchange для типизированных массивов в SharedArrayBuffer.
    // работает с Int32Array
    const sab = new SharedArrayBuffer(4);
    const a = new Int32Array(sab);
    console.log(Atomics.compareExchange(a, 0, 0, 1));
    0
  • Python: в стандартной библиотеке нет прямого CAS для произвольных ссылок. Примеры через C-расширения, модуль multiprocessing.Value с локами или сторонние библиотеки; также можно использовать atomicwrites и lock-based паттерны.
    # нет нативного CAS для объектов в CPython; пример с lock
    from threading import Lock
    class AtomicRef:
        def __init__(self, v):
            self._v = v
            self._l = Lock()
        def compare_and_set(self, expect, update):
            with self._l:
                if self._v is expect:
                    self._v = update
                    return True
                return False
    
    (работает с блокировкой, но не является неблокирующей)
  • PHP: нет встроенного CAS для объектов; можно использовать расширения, POSIX-совместимые примитивы или пользовательские примитивные конструкции с разделяемой памятью и семафорами.
  • Lua: стандартно нет; для конкурентных реализаций используются C-биндинги или внешние примитивы.
  • Kotlin: на JVM можно использовать те же AtomicReference из Java или Kotlin-обёртки из kotlin.concurrent и kotlinx.atomicfu, последний предоставляет DSL и компиляторные оптимизации.
  • SQL: базы данных предлагают атомарные операции через транзакции, UPDATE ... WHERE с проверкой старого значения или конструкции типа SELECT ... FOR UPDATE, что отличается от CAS по механике (блокировки/транзакции вместо CPU-операции CAS).

Типичные ошибки и примеры

  • Ожидание сравнения по equals вместо ссылки. Частая ошибка - ожидание, что compareAndSet сравнивает по equals(). На деле сравнение по ссылке, поэтому два равных объектов могут не совпадать.
    AtomicReference r = new AtomicReference<>(new String("X"));
    boolean ok = r.compareAndSet(new String("X"), "Y");
    System.out.println(ok + ", value=" + r.get());
    false, value=X
  • Использование автопаковки Integer как ключа без учёта кэширования может давать непредсказуемые результаты: одинаковые значения в диапазоне кэша могут быть одним объектом, а вне диапазона - разными ссылками (см. секцию примеров).
  • Ожидание отсутствия ABA-проблемы. CAS не защищает от ситуации, когда значение поменялось с A на B и обратно в A; внешне CAS видит A и позволяет операцию, но логика может быть нарушена. Решение: использовать AtomicStampedReference или версионирование.
  • Игнорирование возможных спонтанных неудач при weakCompareAndSet.
  • Попытки использовать CAS для сложных операций без цикла перезапуска: многие обновления требуют повторения попытки при конфликте.

Изменения и расширения в последних версиях Java

Начиная с Java 9 и далее в стандартной библиотеке появились дополнительные методы и варианты по семантике памяти:

  • Добавлены методы наподобие compareAndExchange, compareAndExchangeAcquire и compareAndExchangeRelease в AtomicReference для возвращения предыдущего значения при попытке замены и для управления режимом памяти.
  • Появились более тонкие варианты слабых операций (weakCompareAndSetPlain, weakCompareAndSetAcquire и т.д.) в ряде классов и VarHandle API для тонкой настройки семантики видимости.
  • VarHandle предоставляет аналогичные CAS-операции на полях и массивах с указанием режимов памяти, что даёт большую гибкость для оптимизаций.

Явных изменений в поведении compareAndSet не было; добавлены расширения для новых режимов и API.

Расширенные и редкие сценарии использования

Пример 1: неблокирующий стек (Treiber stack) с использованием AtomicReference<Node>.

Пример java
import java.util.concurrent.atomic.AtomicReference;

class Node { final int value; final Node next; Node(int v, Node n){ value=v; next=n; }}

public class TreiberStack {
    private final AtomicReference top = new AtomicReference<>(null);

    public void push(int v){
        Node newNode;
        do {
            Node cur = top.get();
            newNode = new Node(v, cur);
        } while (!top.compareAndSet(newNode.next, newNode));
    }

    public Integer pop(){
        Node cur;
        do {
            cur = top.get();
            if (cur == null) return null;
        } while (!top.compareAndSet(cur, cur.next));
        return cur.value;
    }

    public static void main(String[] args){
        TreiberStack s = new TreiberStack();
        s.push(1); s.push(2);
        System.out.println(s.pop());
        System.out.println(s.pop());
    }
}
2
1

Комментарий: стек неблокирующий, но подвержен ABA; при необходимости добавить штамп версии.

Пример 2: защита от ABA с AtomicStampedReference

Пример java
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAExample {
    public static void main(String[] args){
        AtomicStampedReference r = new AtomicStampedReference<>("A", 0);
        int[] stamp = new int[1];
        String v = r.get(stamp);
        int s = stamp[0];
        // другой поток поменял A -> B -> A, увеличив штамп
        r.set("A", s+1);
        boolean ok = r.compareAndSet(v, "C", s, s+2);
        System.out.println(ok + ", value=" + r.getReference() + ", stamp=" + r.getStamp());
    }
}
false, value=A, stamp=1

Пример 3: ленивый инициализатор без блоков

Пример java
import java.util.concurrent.atomic.AtomicReference;

public class LazyInit {
    private final AtomicReference ref = new AtomicReference<>();
    private final java.util.function.Supplier supplier;
    public LazyInit(java.util.function.Supplier s){ supplier = s; }
    public T get(){
        T v = ref.get();
        if (v != null) return v;
        T newVal = supplier.get();
        if (ref.compareAndSet(null, newVal)) return newVal;
        else return ref.get();
    }
}

// Результат: объект создаётся один раз конкурентно без synchronized
(создаёт объект один раз, возвращает ссылку на инициализированный объект)

Пример 4: использование weakCompareAndSet в tight-loop

Пример java
AtomicReference r = new AtomicReference<>("x");
boolean success;
do {
    String cur = r.get();
    String next = cur + "!";
    success = r.weakCompareAndSet(cur, next);
} while (!success);
// weakCompareAndSet может возвращать false спонтанно, поэтому цикл необходим
(в итоге значение обновлено, но могут быть дополнительные повторы)

Пример 5: использование compareAndExchange (Java 9+) для получения старого значения

Пример java
// Требует Java 9+
import java.util.concurrent.atomic.AtomicReference;

public class ExchangeDemo {
    public static void main(String[] args){
        AtomicReference r = new AtomicReference<>("old");
        String prev = r.compareAndExchange("old", "new");
        System.out.println(prev + ", now=" + r.get());
    }
}
old, now=new

джава AtomicReference.compareAndSet function comments

En
AtomicReference.compareAndSet Сравнить и установить (CAS операция)