Введение в атомарные переменные в Java

Введение в атомарные переменные в Java

1. Вступление

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

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

2. Замки

Посмотрим на класс:

public class Counter {
    int counter;

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

В случае однопоточной среды это работает отлично; однако, как только мы разрешаем писать более чем одному потоку, мы начинаем получать противоречивые результаты.

Это происходит из-за простой операции приращения (counter++), которая может выглядеть как атомарная операция, но на самом деле представляет собой комбинацию трех операций: получение значения, увеличение и обратная запись обновленного значения.

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

Одним из способов управления доступом к объекту является использование блокировок. Этого можно достичь, используя ключевое словоsynchronized в сигнатуре методаincrement. Ключевое словоsynchronized гарантирует, что только один поток может войти в метод одновременно (чтобы узнать больше о блокировке и синхронизации, обратитесь к -Guide to Synchronized Keyword in Java):

public class SafeCounterWithLock {
    private volatile int counter;

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

Кроме того, нам нужно добавить ключевое словоvolatile, чтобы обеспечить надлежащую видимость ссылок среди потоков.

Использование замков решает проблему. Однако производительность страдает.

Когда несколько потоков пытаются получить блокировку, один из них выигрывает, а остальные потоки либо блокируются, либо приостанавливаются.

The process of suspending and then resuming a thread is very expensive и влияет на общую эффективность системы.

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

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

Существует направление исследований, направленных на создание неблокирующих алгоритмов для параллельных сред. Эти алгоритмы используют низкоуровневые атомарные машинные инструкции, такие как сравнение и замена (CAS), для обеспечения целостности данных.

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

  1. Место памяти, на котором можно оперировать (М)

  2. Существующее ожидаемое значение (A) переменной

  3. Новое значение (B), которое необходимо установить

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

В обоих случаях возвращается существующее значение в M. Это объединяет три шага - получение значения, сравнение значения и обновление значения - в одну операцию на уровне компьютера.

Когда несколько потоков пытаются обновить одно и то же значение через CAS, один из них выигрывает и обновляет значение. However, unlike in the case of locks, no other thread gets suspended; вместо этого им просто сообщают, что им не удалось обновить значение. Затем потоки могут приступить к дальнейшей работе, и переключение контекста полностью исключается.

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

4. Атомарные переменные в Java

Наиболее часто используемые классы атомарных переменных в Java - этоAtomicInteger,AtomicLong,AtomicBoolean иAtomicReference. Эти классы представляют собойint,long,boolean и ссылку на объект соответственно, которые можно обновлять атомарно. Основные методы, предоставляемые этими классами:

  • get() - получает значение из памяти, чтобы изменения, сделанные другими потоками, были видны; эквивалентно чтению переменнойvolatile

  • set() - записывает значение в память, чтобы изменение было видно другим потокам; эквивалентно записи переменнойvolatile

  • lazySet() - со временем записывает значение в память, может быть переупорядочено с последующими соответствующими операциями с памятью. Один из вариантов использования - аннулирование ссылок для сбора мусора, к которому никогда больше не будет доступа. В этом случае лучшая производительность достигается за счет задержки записи нулевогоvolatile

  • compareAndSet() - то же, что описано в разделе 3, возвращает истину в случае успеха, иначе ложь

  • weakCompareAndSet() - то же, что описано в разделе 3, но слабее в том смысле, что не создает упорядочения «происходит до». Это означает, что он не обязательно видит обновления, сделанные для других переменных

Поточно-безопасный счетчик, реализованный с помощьюAtomicInteger, показан в примере ниже:

public class SafeCounterWithoutLock {
    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

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

5. Заключение

В этом кратком руководстве мы описали альтернативный способ обработки параллелизма, где можно избежать недостатков, связанных с блокировкой. Мы также рассмотрели основные методы, предоставляемые классами атомарных переменных в Java.

Как всегда, доступны все примерыover on GitHub.

Чтобы изучить больше классов, которые внутренне используют неблокирующие алгоритмы, обратитесь кa guide to ConcurrentMap.