JavaRush /Курсы /JAVA 25 SELF /AtomicInteger, AtomicReference: атомарные операции

AtomicInteger, AtomicReference: атомарные операции

JAVA 25 SELF
53 уровень , 3 лекция
Открыта

1. Почему i++ не работает в многопоточности?

Начнём с классической задачи: у нас есть переменная‑счётчик, например, количество обработанных заявок или число скачанных файлов. Мы хотим, чтобы несколько потоков увеличивали этот счётчик. Что может пойти не так, если просто написать i++?

Пример: гонка данных на инкременте

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // Не атомарно!
    }
}

Представим, что два потока одновременно вызывают increment(). Оба потока читают старое значение, оба увеличивают его на 1, и оба записывают… одно и то же новое значение! В результате один инкремент «теряется». Если повторять это много раз, итоговое значение будет меньше ожидаемого.

Почему так происходит?
Операция i++ на самом деле состоит из трёх шагов:

  1. Прочитать значение переменной (например, 5).
  2. Увеличить это значение на 1.
  3. Записать новое значение обратно в память.

А в многопоточной среде другой поток может успеть изменить переменную между этими шагами. Итог — «гонка данных» (race condition).

Что такое атомарные операции?

Атомарная операция — это действие, которое либо выполняется полностью, либо не выполняется вовсе, и никакой другой поток не может «вклиниться» в середину этой операции.

В Java есть набор классов, предоставляющих такие операции для примитивов и ссылок. Они расположены в пакете java.util.concurrent.atomic. Самые популярные:

  • AtomicInteger — атомарный целочисленный тип.
  • AtomicLong — атомарный long.
  • AtomicBoolean — атомарный boolean.
  • AtomicReference<T> — атомарная ссылка на объект любого типа.

2. AtomicInteger: потокобезопасный счётчик

Объявление и базовое использование

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Атомарное увеличение
    }

    public int get() {
        return count.get();
    }
}

Здесь incrementAndGet() выполняет «увеличить и вернуть новое значение» как одну неделимую операцию. Даже если 100 потоков одновременно вызовут этот метод, ни один инкремент не потеряется.

Полезные методы:

Метод Описание
get()
Получить текущее значение
set(int value)
Установить значение
incrementAndGet()
Увеличить на 1 и вернуть новое значение
getAndIncrement()
Вернуть текущее значение и увеличить на 1
addAndGet(int delta)
Увеличить на delta и вернуть новое значение
compareAndSet(expect, update)
Если текущее значение равно expect, установить update (CAS)

Пример: многопоточный счётчик

Пусть у нас есть класс, который считает количество обработанных сообщений в чате.

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("Всего сообщений: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

Внутренности: как работает AtomicInteger?

Внутри AtomicInteger использует специальную инструкцию процессора — CAS (Compare-And-Swap, «сравнить‑и‑подменить»). Это атомарная операция, которая сравнивает текущее значение переменной с ожидаемым, и если они совпадают — записывает новое значение. Если другой поток успел изменить переменную — операция не выполняется, и попытка повторяется.

Схема работы:

1. Читаем текущее значение (например, 5)
2. Сравниваем с ожидаемым (5)
3. Если совпадает — записываем новое значение (6)
4. Если не совпадает — повторяем попытку

Всё это происходит очень быстро и без блокировок (lock‑free). Поэтому атомарные классы часто быстрее, чем synchronized, особенно при большом количестве потоков.

3. AtomicReference: атомарная ссылка на объект

AtomicReference<T> — это универсальный атомарный контейнер для любого объекта. Он позволяет безопасно менять ссылку на объект из разных потоков.

Пример: потокобезопасное обновление ссылки

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

Применение compareAndSet

Самая интересная операция — compareAndSet(expected, newValue). Она позволяет обновить значение только если оно не изменилось с момента последнего чтения.

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("Обновление прошло успешно!");
    } else {
        System.out.println("Кто-то уже изменил значение, попробуйте ещё раз.");
    }
}

Это основа неблокирующих алгоритмов: от очередей и стеков до кэшей, где важно избежать лишних блокировок.

4. Примеры использования в приложении

Пример 1: многопоточный счётчик сообщений

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... обработка сообщения ...
        int count = messageCount.incrementAndGet();
        System.out.println("Новое сообщение: " + message + ". Всего сообщений: " + count);
    }
}

Пример 2: безопасное обновление ссылки на последнее сообщение

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... обработка ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

Если нужно обновлять ссылку только если последнее сообщение не изменилось (чтобы избежать «потери» при одновременных обновлениях), используйте compareAndSet.

5. Ограничения и подводные камни

Когда атомарные классы — не панацея?

Атомарные переменные отлично подходят для простых операций: инкремент, установка значения, проверка и замена. Но если нужно обновить несколько переменных одновременно, атомарность уже не гарантируется. Например, если у вас два счётчика и вы хотите увеличить их оба как одну операцию — здесь нужен synchronized или другой механизм синхронизации.

Пример неправильного использования

// НЕ атомарно!
if (ref.get() == null) {
    ref.set("Hello");
}

Между get() и set(...) другой поток может изменить значение, и условие уже будет неверным. Для таких случаев используйте compareAndSet.

Атомарные классы ≠ потокобезопасные объекты

Если объект, на который указывает AtomicReference, сам не потокобезопасен, то замена ссылки будет атомарной, но изменение полей объекта — нет. Например, если вы храните в AtomicReference<List<String>> обычный ArrayList, то сам список не станет thread‑safe.

6. Продвинутые атомарные классы

В пакете java.util.concurrent.atomic есть и другие полезные классы:

  • AtomicLong, AtomicBoolean — для long и boolean.
  • AtomicIntegerArray, AtomicReferenceArray — атомарные операции над массивами.
  • LongAdder, LongAccumulator — для высоконагруженных счётчиков.

LongAdder и LongAccumulator

Если у вас очень много потоков и обычный AtomicInteger становится «узким горлышком» (все потоки конкурируют за одну переменную), используйте LongAdder. Он разбивает счётчик на несколько внутренних ячеек и суммирует их при запросе значения, что даёт выигрыш при высокой конкуренции.

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. Типичные ошибки при работе с атомарными переменными

Ошибка №1: Ожидание атомарности сложных операций.
Если нужно выполнить несколько действий над значением, атомарные классы не спасут — между шагами другой поток может изменить данные. Для составных операций используйте compareAndSet или синхронизацию.

Ошибка №2: Игнорирование thread‑safety вложенных объектов.
Если в AtomicReference лежит обычный объект, его методы и поля не становятся потокобезопасными. Атомарна лишь замена ссылки.

Ошибка №3: Использование атомарных классов без необходимости.
В однопоточном коде атомарные типы избыточны и немного медленнее обычных переменных из‑за дополнительных проверок.

Ошибка №4: Преждевременная оптимизация.
Иногда проще и надёжнее использовать synchronized, особенно когда логика сложная и затрагивает несколько переменных сразу. Не всегда оправдано строить lock‑free решения.

Ошибка №5: Забыли про ABA‑проблему.
Редкий, но важный случай: значение меняется с A на B и снова на A — compareAndSet «думает», что ничего не менялось. Для таких сценариев используйте спецклассы вроде AtomicStampedReference (или AtomicMarkableReference).

1
Задача
JAVA 25 SELF, 53 уровень, 3 лекция
Недоступна
Подсчёт посетителей на мега-событии: Потокобезопасный счётчик 🏟️
Подсчёт посетителей на мега-событии: Потокобезопасный счётчик 🏟️
1
Задача
JAVA 25 SELF, 53 уровень, 3 лекция
Недоступна
Обновление текущего лидера гильдии: Безопасное обновление объекта с AtomicReference 👑
Обновление текущего лидера гильдии: Безопасное обновление объекта с AtomicReference 👑
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ