Atomic - принцип работы

Atomic классы в Java, такие как AtomicInteger, AtomicLong, AtomicReference и т.д., предоставляют атомарные операции чтения и записи для соответствующих типов данных. Они позволяют избежать гонок данных (data races) путем гарантированного выполнения операций в единой атомарной операции. Вот основные особенности и принципы работы под капотом Atomic классов:

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

    • Atomic классы предоставляют методы для атомарных операций чтения и записи, таких как getAndIncrement(), compareAndSet(), getAndSet() и другие.
    • Эти операции выполняются непрерывно, не допуская переключения контекста между потоками в середине операции.
  2. Использование Compare-and-Swap (CAS):

    • Базовым механизмом, используемым в Atomic классах, является алгоритм Compare-and-Swap.
    • Этот алгоритм сравнивает текущее значение переменной с ожидаемым значением и, если они совпадают, обновляет значение переменной на новое.
    • Эта операция выполняется атомарно и может быть повторена в случае неудачи.
  3. Примитивы памяти:

    • Atomic классы также используют примитивы памяти, такие как volatile, чтобы гарантировать видимость изменений переменных между потоками.
    • volatile обеспечивает, что операции чтения и записи к переменной будут видны всем потокам независимо от кеша потока.
  4. Lock-Free алгоритмы:

    • Многие операции в Atomic классах реализованы с использованием lock-free алгоритмов.
    • Это позволяет избежать ожидания блокировок, что может привести к более эффективной работе в условиях высокой нагрузки и уменьшить вероятность возникновения взаимных блокировок.
  5. Оптимизация памяти и производительности:

    • Atomic классы обычно оптимизированы для минимального использования памяти и максимальной производительности.
    • Например, для AtomicInteger в некоторых реализациях используется специальный характеристический бит, который позволяет уменьшить количество CAS операций в определенных сценариях использования.
  6. Платформозависимая реализация:

    • Реализация Atomic классов может отличаться в зависимости от платформы и JVM.
    • Например, на некоторых архитектурах атомарные операции могут быть реализованы с использованием низкоуровневых инструкций процессора, тогда как на других архитектурах они могут быть реализованы через CAS примитивы.
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);

        // Поток инкрементирует счётчик 1000 раз
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet(); // Атомарная операция инкремента
            }
        });

        // Поток декрементирует счётчик 1000 раз
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.decrementAndGet(); // Атомарная операция декремента
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter value: " + counter.get());
    }
}

В этом примере AtomicInteger используется для подсчёта суммы инкрементов и декрементов из двух параллельных потоков. Методы incrementAndGet() и decrementAndGet() предоставляют атомарные операции инкремента и декремента, соответственно. Это означает, что каждая операция инкремента или декремента будет выполнена целиком до того, как другой поток сможет выполнить свою операцию, и гонки данных не произойдет.