Атомарные классы пакета util.concurrent

Пакет java.util.concurrent.atomic содержит девять классов для выполнения атомарных операций. Операция называется атомарной, если её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни синхронизацию synchronized. Прежде, чем перейти к рассмотрению атомарных классов, рассмотрим выполнение наипростейших операций инкремента и декремента целочисленных значений.

С точки зрения программиста операции инкремента (i++, ++i) и декремента (i--, --i) выглядят наглядно и компактно. Но, с точки зрения JVM (виртуальной машины Java) данные операции не являются атомарными, поскольку требуют выполнения нескольких действительно атомарных операции: чтение текущего значения, выполнение инкремента/декремента и запись полученного результата. При работе в многопоточной среде операции инкремента и декремента могут стать источником ошибок. Т.е. в многопоточной среде простые с виду операции инкремента и декремента требуют использование синхронизации и блокировки. Но блокировки содержат массу недостатков, и для простейших операций инкремента/декремента являются тяжеловесными. Выполнение блокировки связано со средствами операционной системы и несёт в себе опасность приостановки с невозможностью дальнейшего возобновления потока, а также опасность взаимоблокировки или инверсии приоритетов (priority inversion). Кроме этого, появляются дополнительные расходы на переключение потоков. Но можно ли обойтись без блокировок? В ряде случаев можно!

Блокировка подразумевает пессимистический подход, разрешая только одному потоку выполнять определенный код, связанный с изменением значения некоторой «общей» переменной. Таким образом, никакой другой поток не имеет доступа к определенным переменным. Но можно использовать и оптимистический подход. В этом случае блокировки не происходит, и если поток обнаруживает, что значение переменной изменилось другим потоком, то он повторяет операцию снова, но уже с новым значением переменной. Так работают атомарные классы.

Описание атомарного класса AtomicLong

Рассмотрим принцип действия механизма оптимистической блокировки на примере атомарного класса AtomicLong, исходный код которого представлен ниже. В этом классе переменная value объявлена с модификатором volatile, т.е. её значение могут поменять разные потоки одновременно. Модификатор volatile гарантирует выполнение отношения happens-before, что ведет к тому, что измененное значение этой переменной увидят все потоки.

Каждый атомарный класс включает метод compareAndSet, представляющий механизм оптимистичной блокировки и позволяющий изменить значение value только в том случае, если оно равно ожидаемому значению (т.е. current). Если значение value было изменено в другом потоке, то оно не будет равно ожидаемому значению. Следовательно, метод compareAndSet вернет значение false, что приведет к новой итерации цикла while в методе getAndAdd. Таким образом, в очередном цикле в переменную current будет считано обновленное значение value, после чего будет выполнено сложение и новая попытка записи получившегося значения (т.е. next). Переменные current и next - локальные, и, следовательно, у каждого потока свои экземпляры этих переменных.

private volatile long value;
 
public final long get() {
    return value;
}
 
public final long getAndAdd(long delta) {
    while (true) {
        long current = get();
        long next = current + delta;
        if (compareAndSet(current, next))
            return current;
    }
}

Метод compareAndSet реализует механизм оптимистической блокировки. Знакомые с набором команд процессоров специалисты знают, что ряд архитектур имеют инструкцию Compare-And-Swap (CAS), которая является реализацией этой самой операции. Таким образом, на уровне инструкций процессора имеется поддержка необходимой атомарной операции. На архитектурах, где инструкция не поддерживается, операции реализованы иными низкоуровневыми средствами.

Основная выгода от атомарных (CAS) операций появляется только при условии, когда переключать контекст процессора с потока на поток становится менее выгодно, чем немного покрутиться в цикле while, выполняя метод boolean compareAndSwap(oldValue, newValue). Если время, потраченное в этом цикле, превышает 1 квант потока, то, с точки зрения производительности, может быть невыгодно использовать атомарные переменные.

Список атомарных классов

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

• AtomicBoolean
• AtomicInteger
• AtomicLong
• AtomicReference
Atomic-классы для boolean, integer, long и ссылок на объекты.
Классы этой группы содержат метод compareAndSet, принимающий 2 аргумента : предполагаемое текущее и новое значения. Метод устанавливает объекту новое значение, если текущее равно предполагаемому, и возвращает true. Если текущее значение изменилось, то метод вернет false и новое значение не будет установлено.
Кроме этого, классы имеют метод getAndSet, который безусловно устанавливает новое значение и возвращает старое.
Классы AtomicInteger и AtomicLong имеют также методы инкремента/декремента/добавления нового значения.
• AtomicIntegerArray
• AtomicLongArray
• AtomicReferenceArray
Atomic-классы для массивов integer, long и ссылок на объекты.
Элементы массивов могут быть изменены атомарно.
• AtomicIntegerFieldUpdater
• AtomicLongFieldUpdater
• AtomicReferenceFieldUpdater
Atomic-классы для обновления полей по их именам с использованием reflection.
Смещения полей для CAS операций определяется в конструкторе и кэшируются. Сильного падения производительности из-за reflection не наблюдается.
• AtomicStampedReference
• AtomicMarkableReference
Atomic-классы для реализации некоторых алгоритмов, (точнее сказать, уход от проблем при реализации алгоритмов).
Класс AtomicStampedReference получает в качестве параметров ссылку на объект и int значение.
Класс AtomicMarkableReference получает в качестве параметров ссылку на объект и битовый флаг (true/false).

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

Производительность атомарных классов

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

Основной недостаток неблокирующих алгоритмов связан со сложностью их реализации по сравнению с блокировками. Особенно это касается ситуаций, когда необходимо контролировать состояние не одного поля, а нескольких.

Пример неблокирующего генератора последовательности

Рассмотрим генерирующий последовательность [1, 2, 4, 8, 16, ...] класс SequenceGenerator, функционирующий в многопоточной среде.

Листинг класса SequenceGenerator для генерирования последовательности

Для работы в многопоточной среде без блокировок используем атомарную ссылку AtomicReference, которая обеспечит хранение целочисленного значения типа java.math.BigInteger. Метод next возвращает текущее значение; переменная next вычисляет следующее значение. Метод compareAndSet атомарного класса element обеспечивает сохранение нового значения, если текущее не изменилось. Таким образом, метод next возвращает текущее значение и увеличивает его в 2 раза.

import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicReference;

public class SequenceGenerator 
{
    private static BigInteger           MULTIPLIER;
    private AtomicReference<BigInteger> element;

    public SequenceGenerator()
    {
        if (MULTIPLIER == null)
            MULTIPLIER = BigInteger.valueOf(2);
        element = new AtomicReference<BigInteger>(BigInteger.ONE);
    }
    public BigInteger next() 
    {
        BigInteger value;
        BigInteger next;
        do {
            value = element.get();
            next = value.multiply(MULTIPLIER);
        } while (!element.compareAndSet(value, next));
        return value;
    }
}

Листинг последовательности Sequence

Для тестирования генератора последовательности SequenceGenerator используем класс Sequence, реализующий интерфейс Runnable. В качестве параметра конструктор класса получает идентификатор потока id, размер последовательности count и генератор последовательности sg. В методе run в цикле с незначительными задержками формируется последовательность чисел sequence. После завершения цикла значения последовательности «выводятся» в консоль методом printSequence.

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

class Sequence implements Runnable
{
    Thread thread;
    int id;
    int count;
    SequenceGenerator sg;
    List<BigInteger> sequence = new ArrayList<BigInteger>();
    boolean printed = false;
    
    Sequence(final int id, final int count, SequenceGenerator sg) 
    {
        this.count = count;
        this.id    = id;
        this.sg    = sg;
        thread     = new Thread(this);

        System.out.println("Создан поток " + id);
        thread.start();
    }
    @Override
    public void run() {
        try {
            for (int i = 0; i < count; i++) {
                sequence.add(sg.next());
                Thread.sleep((long) ((Math.random()*2 + 1)*30));
            }
        } catch (InterruptedException e) {
            System.out.println("Поток " + id + " прерван");
        }
        System.out.print("Поток " + id + " завершён");
        printSequence();
    }
    public void printSequence()
    {
        if (printed)
            return;
        String tmp = "[";
        for (int i = 0; i < sequence.size(); i++) {
            if (i > 0)
                tmp += ", ";
            String nb = String.valueOf(sequence.get(i));
            while (nb.length() < 9)
                nb = " " + nb;
            tmp += nb;
        }
        tmp += "]";
        System.out.println("Последовательность потока " + 
                           id + " : " + tmp);
        printed = true;
    }
}

Листинг примера SequenceGeneratorExample

В примере SequenceGeneratorExample сначала создается генератор последовательности SequenceGenerator. После этого в цикле формируется массив из десяти Sequence, которые в паралелльных потоках по три раза обращаются к генератору последовательсности.

public class SequenceGeneratorExample
{
    public static void main(String[] args) 
    {
        SequenceGenerator sg = new SequenceGenerator();
        List<Sequence> sequences = new ArrayList<Sequence>();
        for (int i = 0; i < 10; i++) {
            Sequence seq = new Sequence(i + 1, 3, sg);
            sequences.add(seq);
        }
        System.out.println("\nРасчет последовательностей\n");
        int summa;
        // Ожидания завершения потоков
        do {
            summa = 0;
            for (int i = 0; i < sequences.size(); i++) {
                if (!sequences.get(i).thread.isAlive()) {
                    sequences.get(i).printSequence();
                    summa++;
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
        } while (summa < sequences.size()) ;
        System.out.println("\n\nРабота потоков завершена");
        System.exit(0);
    }
}

Результаты выполнения примера

При выполнении примера в консоль будет выведена следующая информация :


Создан поток 0
Создан поток 1
Создан поток 2
Создан поток 3
Создан поток 4
Создан поток 5
Создан поток 6
Создан поток 7
Создан поток 8
Создан поток 9

Расчет последовательностей

Поток 7 завершён
Последовательность потока 7 :  [ 256,   4096,    524288]
Поток 5 завершён
Поток 4 завершён
Поток 1 завершён
Последовательность потока 1 : [   2,    1024,   2097152]
Последовательность потока 4 : [  16,    8192,   8388608]
Последовательность потока 5 : [  64,    2048,     32768]
Поток 9 завершён
Поток 3 завершён
Поток 6 завершён
Последовательность потока 3 : [   8,  131072, 134217728]
Последовательность потока 6 : [  32,   16384, 268435456]
Последовательность потока 9 : [ 512,  262144,  16777216]
Поток 0 завершён
Поток 2 завершён
Поток 8 завершён
Последовательность потока 0 : [   1,   65536,  67108864]
Последовательность потока 2 : [   4, 1048576,  33554432]
Последовательность потока 8 : [ 128, 4194304, 536870912]

Работа потоков завершена

 

Каждый поток в цикле сформировал целочисленный массив из 3-х значений при обращении к «атомарному» генератору последовательности. Как видно из результатов выполнения примера, значения не пересекаются.

Скачать примеры

Рассмотренный на странице пример использования атомарного класса в виде проекта Eclipse можно скачать здесь (7.41 Кб).

  Рейтинг@Mail.ru