Практическое использование для AtomicInteger
Я вроде понимаю, что AtomicInteger и другие переменные Atomic допускают одновременный доступ. В каких случаях этот класс обычно используется?
13 ответов
Есть два основных использования AtomicInteger
:
В качестве атомного счетчика (
incrementAndGet()
и т. д.) которые могут использоваться многими потоками одновременноВ качестве примитива, который поддерживает инструкцию сравнения и обмена (
compareAndSet()
) реализовать неблокирующие алгоритмы.Вот пример неблокирующего генератора случайных чисел из Java-параллелизма Брайана Гетца на практике:
public class AtomicPseudoRandom extends PseudoRandom { private AtomicInteger seed; AtomicPseudoRandom(int seed) { this.seed = new AtomicInteger(seed); } public int nextInt(int n) { while (true) { int s = seed.get(); int nextSeed = calculateNext(s); if (seed.compareAndSet(s, nextSeed)) { int remainder = s % n; return remainder > 0 ? remainder : remainder + n; } } } ... }
Как вы можете видеть, это в основном работает почти так же, как
incrementAndGet()
, но выполняет произвольный расчет (calculateNext()
) вместо приращения (и обрабатывает результат перед возвратом).
Абсолютно простейший пример, который я могу придумать, - сделать инкрементную атомарную операцию
Со стандартными целями:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
С AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Последнее является очень простым способом выполнения простых эффектов мутаций (особенно подсчета или уникальной индексации), без необходимости прибегать к синхронизации всего доступа.
Более сложная логика без синхронизации может быть использована с помощью compareAndSet()
как тип оптимистической блокировки - получите текущее значение, вычислите результат на основе этого, установите этот результат, если значение все еще является входным значением, используемым для вычисления, иначе начните снова - но примеры подсчета очень полезны, и я буду часто использование AtomicIntegers
для подсчета и генераторов уникальных для всей VM, если есть какой-то намек на участие нескольких потоков, потому что с ними так легко работать, что я почти считаю преждевременной оптимизацию, чтобы использовать простой ints
,
Хотя почти всегда можно добиться одинаковых гарантий синхронизации с ints
и соответствующий synchronized
декларации, красота AtomicInteger
заключается в том, что безопасность потока встроена в сам фактический объект, и вам не нужно беспокоиться о возможных чередованиях и удерживаемых мониторах каждого метода, который происходит с доступом к int
значение. Гораздо сложнее случайно нарушить безопасность потоков при вызове getAndIncrement()
чем при возвращении i++
и запоминание (или нет), чтобы получить правильный набор мониторов заранее.
Если вы посмотрите на методы, которые есть у AtomicInteger, вы заметите, что они, как правило, соответствуют обычным операциям над целыми числами. Например:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
является потокобезопасной версией этого:
static int i;
// Later, in a thread
int current = ++i;
Методы отображаются так:++i
является i.incrementAndGet()
i++
является i.getAndIncrement()
--i
является i.decrementAndGet()
i--
является i.getAndDecrement()
i = x
является i.set(x)
x = i
является x = i.get()
Есть и другие удобные методы, такие как compareAndSet
или же addAndGet
Основное использование AtomicInteger
когда вы находитесь в многопоточном контексте, и вам нужно выполнять потокобезопасные операции над целым числом, не используя synchronized
, Назначение и поиск по типу примитива int
уже атомные, но AtomicInteger
поставляется со многими операциями, которые не являются атомарными на int
,
Самые простые getAndXXX
или же xXXAndGet
, Например getAndIncrement()
атомный эквивалент i++
что не является атомарным, потому что это на самом деле сокращение для трех операций: поиск, добавление и присваивание. compareAndSet
очень полезно для реализации семафоров, замков, защелок и т. д.
С использованием AtomicInteger
это быстрее и более читабельно, чем выполнять то же самое с использованием синхронизации.
Простой тест:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
На моем ПК с Java 1.6 атомарный тест выполняется за 3 секунды, а синхронизированный - около 5,5 секунд. Проблема здесь в том, что операция для синхронизации (notAtomic++
) действительно короткий. Поэтому стоимость синхронизации действительно важна по сравнению с операцией.
Помимо атомарности AtomicInteger можно использовать как изменчивую версию Integer
например в Map
s как значения.
Например, у меня есть библиотека, которая генерирует экземпляры некоторого класса. Каждый из этих экземпляров должен иметь уникальный целочисленный идентификатор, поскольку эти экземпляры представляют команды, отправляемые на сервер, и каждая команда должна иметь уникальный идентификатор. Поскольку нескольким потокам разрешено отправлять команды одновременно, я использую AtomicInteger для генерации этих идентификаторов. Альтернативным подходом может быть использование некоторого типа блокировки и обычного целого числа, но это и медленнее, и менее элегантно.
В Java 8 атомарные классы были расширены двумя интересными функциями:
- int getAndUpdate (IntUnaryOperator updateFunction)
- int updateAndGet (функция обновления IntUnaryOperator)
Оба используют функцию updateFunction для обновления атомарного значения. Разница в том, что первый возвращает старое значение, а второй возвращает новое значение. Функция updateFunction может быть реализована для выполнения более сложных операций "сравнить и установить", чем стандартная. Например, он может проверить, что атомный счетчик не опускается ниже нуля, обычно это требует синхронизации, и здесь код не блокируется:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
Код взят из Java Atomic Example.
Как сказал Габузо, иногда я использую AtomicIntegers, когда хочу передать int по ссылке. Это встроенный класс, в котором есть специфичный для архитектуры код, поэтому он проще и, вероятно, более оптимизирован, чем любой MutableInteger, который я мог бы быстро кодировать. Тем не менее, это похоже на злоупотребление класса.
Я обычно использую AtomicInteger, когда мне нужно дать идентификаторы объектам, которые могут быть приняты или созданы из нескольких потоков, и я обычно использую его как статический атрибут в классе, к которому я обращаюсь в конструкторе объектов.
Вы можете реализовать неблокирующие блокировки, используя compareAndSwap (CAS) для атомарных целых или длинных значений. В документе "Транзакционная память программного обеспечения Tl2" описывается это:
Мы связываем специальную версионную блокировку записи с каждой транзакционной ячейкой памяти. В простейшей форме версионная блокировка записи представляет собой спин-блокировку из одного слова, которая использует операцию CAS для получения блокировки и хранилище для ее снятия. Поскольку требуется только один бит, чтобы указать, что блокировка снята, мы используем оставшуюся часть слова блокировки для хранения номера версии.
То, что он описывает, это сначала прочитать атомное целое число. Разделите это на игнорируемый бит блокировки и номер версии. Попытайтесь записать его в CAS как бит блокировки, очищенный с текущим номером версии, в установленный бит блокировки и следующий номер версии. Цикл, пока вы не добьетесь успеха, и ваша нить владеет замком. Разблокируйте, установив номер текущей версии с очищенным битом блокировки. В статье описывается использование номеров версий в замках для координации того, что потоки имеют постоянный набор операций чтения при записи.
В этой статье описывается, что процессоры имеют аппаратную поддержку для операций сравнения и обмена, что делает их очень эффективными. Он также утверждает:
Неблокирующие счетчики на основе CAS, использующие атомарные переменные, имеют лучшую производительность, чем счетчики на основе блокировок, в условиях низкой или средней конкуренции
Ключ в том, что они позволяют одновременный доступ и изменение безопасно. Они обычно используются в качестве счетчиков в многопоточной среде - до их появления это должен был быть класс, написанный пользователем, который упаковывал различные методы в синхронизированные блоки.
Я использовал AtomicInteger для решения проблемы Обедающего Философа.
В моем решении экземпляры AtomicInteger использовались для представления вилок, для каждого философа необходимы два. Каждый Философ идентифицируется как целое число от 1 до 5. Когда философ использует вилку, AtomicInteger содержит значение философа, от 1 до 5, в противном случае вилка не используется, поэтому значение AtomicInteger равно -1.,
Затем AtomicInteger позволяет проверить, свободен ли разветвление, значение ==-1, и установить его для владельца разветвления, если он свободен, за одну атомарную операцию. Смотрите код ниже.
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
Поскольку метод compareAndSet не блокирует, он должен увеличить пропускную способность, сделать больше работы. Как вы, возможно, знаете, проблема "Обедающие философы" используется, когда требуется контролируемый доступ к ресурсам, то есть нужны вилки, как процесс требует ресурсов для продолжения работы.
Атомарные классы не являются заменой общего назначения для java.lang.Integer и связанных с ним классов. Они не определяют такие методы, как equals, hashCode и compareTo. (Поскольку предполагается, что атомарные переменные будут видоизменяться, они являются плохим выбором для ключей хэш-таблицы.) Кроме того, классы предоставляются только для тех типов, которые обычно используются в предполагаемых приложениях. Например, нет атомарного класса для представления байта. В тех редких случаях, когда вы хотели бы это сделать, вы можете использовать AtomicInteger для хранения значений байтов и соответствующего приведения. Вы также можете удерживать числа с плавающей запятой, используя преобразования Float.floatToRawIntBits(float) и Float.intBitsToFloat(int), и удваивать, используя преобразования Double.doubleToRawLongBits(double) и Double.longBitsToDouble(long).
Ссылка: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
Простой пример функции compareAndSet():
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Напечатано следующее: предыдущее значение: 0 Значение было обновлено, и оно равно 6. Другой простой пример:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Напечатано: Предыдущее значение: 0 Значение не обновлялось