- Атомарные классы пакета util.concurrent
- Описание атомарного класса AtomicLong
- Список атомарных классов
- Производительность атомарных классов
- Пример неблокирующего генератора последовательности
- Листинг класса SequenceGenerator для генерирования последовательности
- Листинг последовательности Sequence
- Листинг примера SequenceGeneratorExample
- Результаты выполнения примера
- Скачать примеры
- Atomic Variables
Атомарные классы пакета 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 AtomicReferenceelement; public SequenceGenerator() < if (MULTIPLIER == null) MULTIPLIER = BigInteger.valueOf(2); element = new AtomicReference( 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; Listsequence; sequence = new ArrayList(); 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(); Listsequences = new ArrayList(); 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 Кб).
Atomic Variables
The java.util.concurrent.atomic package defines classes that support atomic operations on single variables. All classes have get and set methods that work like reads and writes on volatile variables. That is, a set has a happens-before relationship with any subsequent get on the same variable. The atomic compareAndSet method also has these memory consistency features, as do the simple atomic arithmetic methods that apply to integer atomic variables.
To see how this package might be used, let’s return to the Counter class we originally used to demonstrate thread interference:
class Counter < private int c = 0; public void increment() < c++; >public void decrement() < c--; >public int value() < return c; >>
One way to make Counter safe from thread interference is to make its methods synchronized, as in SynchronizedCounter :
class SynchronizedCounter < private int c = 0; public synchronized void increment() < c++; >public synchronized void decrement() < c--; >public synchronized int value() < return c; >>
For this simple class, synchronization is an acceptable solution. But for a more complicated class, we might want to avoid the liveness impact of unnecessary synchronization. Replacing the int field with an AtomicInteger allows us to prevent thread interference without resorting to synchronization, as in AtomicCounter :
import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter < private AtomicInteger c = new AtomicInteger(0); public void increment() < c.incrementAndGet(); >public void decrement() < c.decrementAndGet(); >public int value() < return c.get(); >>