Предпосылки появления атомарных операций

Давай разберем данный пример, который поможет понять работу атомарных операций:

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

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

И когда два потока захотят увеличить переменную, скорее всего, ты потеряешь данные. То есть оба потока получают 100, в результате оба запишут 101 вместо ожидаемого значения 102.

И как же это решить? Нужно использовать блокировки. Ключевое слово synchronized помогает решить данную проблему, использование этого слова дает вам гарантию, что один поток будет обращаться к методу единовременно.

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

Плюс надо добавлять ключевое слово volatile, которое обеспечивает корректную видимость ссылок среди потоков. Мы разбирали его работу выше.

Но все же есть минусы. Самый большой — это производительность, в тот момент времени, когда много потоков пытаются получить блокировку и один получает возможность для записи, остальные потоки будут или заблокированы, или приостановлены до момента освобождения потока.

Все эти процессы, блокировка, переход в другой статус ​​— очень дороги для производительности системы.

Атомарные операции

Алгоритм использует низкоуровневые машинные инструкции, такие как сравнение и замена (CAS, compare-and-swap, что обеспечивает целостность данных и по ним уже существует большое количество исследований).

Типичная операция CAS работает с тремя операндами:

  • Место в памяти для работы (M)
  • Существующее ожидаемое значение (A) переменной
  • Новое значение (B), которое необходимо установить

CAS атомарно обновляет M до B, но только если значение M совпадает с A, в противном случае никаких действий предприниматься не будет.

В первом и втором случае вернут значение М. Это позволяет объединить три шага, а именно — получение значения, сравнение значения и его обновление. И это все превращается в одну операцию на машинном уровне.

В тот момент времени, когда многопоточное приложение обращается к переменной и пытается обновить его и применяется CAS, то один из потоков получит его и сможет обновить его. Но в отличии от блокировок, другие потоки просто получат ошибки о том, что им не удалось обновить значение. Потом они перейдут к дальнейшей работе, а переключение полностью исключено при таком типе работе.

При этом логика становится труднее из-за того, что мы должны обработать ситуацию, когда операция CAS не отработала успешно. Мы просто смоделируем код таким образом, чтобы он не двигался дальше, пока операция не произойдет успешно.

Знакомство с атомарными типами

Ты столкнулся с ситуацией, когда тебе нужно настроить синхронизацию для самой простой переменной типа int?

Первый способ, который мы уже разобрали – это использование volatile + synchronized. Но есть еще специальные классы Atomic*.

Если у нас используется CAS, то операции работают быстрее по сравнению с первым способом. И в дополнение у нас есть специальные и очень удобные методы для добавления значения и операции инкремента и декремента.

AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray —классы в которых операции атомарны. Ниже мы разберем работу с ними.

AtomicInteger

Класс AtomicInteger предоставляет операции с значением int, которые могут быть прочитаны и записаны атомарно, в дополнение содержит расширенные атомарные операции.

У него есть методы get и set, которые работают, как чтение и запись по переменным.

То есть “происходит до (happens-before)” с любым последующим получением той же переменной, о которой мы говорили ранее. У атомарного метода compareAndSet также есть эти особенности согласованности памяти.

Все операции, которые возвращают новое значение, выполняются атомарно:

int addAndGet (int delta) Добавляет определенное значение к текущему значению.
boolean compareAndSet (ожидаемое int, обновление int) Устанавливает значение для данного обновленного значения, если текущее значение совпадает с ожидаемым значением.
int decrementAndGet () Уменьшает на единицу текущее значение.
int getAndAdd (int delta) Добавляет данное значение к текущему значению.
int getAndDecrement () Уменьшает на единицу текущее значение.
int getAndIncrement () Увеличивает на единицу текущее значение.
int getAndSet (int newValue) Устанавливает заданное значение и возвращает старое значение.
int incrementAndGet () Увеличивает на единицу текущее значение.
lazySet (int newValue) В конце-концов устанавливается на заданное значение.
boolean weakCompareAndSet (ожидаемое, обновление int) Устанавливает значение для данного обновленного значения, если текущее значение совпадает с ожидаемым значением.

Пример:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // выведет 50