Ответы:
Есть два основных использования 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()
) вместо приращения (и обрабатывает результат перед возвратом).
read
и write that value + 1
операции, это обнаруживается , а не перезаписывать старые обновления (избегая «потерянное обновление» проблему). На самом деле это особый случай compareAndSet
- если старое значение было 2
, класс фактически вызывает compareAndSet(2, 3)
- поэтому, если другой поток изменил значение за это время, метод приращения эффективно перезапускается с самого начала.
Абсолютно простейший пример, который я могу придумать, - сделать инкрементную атомарную операцию
Со стандартными целями:
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 секунд. Проблема в том, что операция synchronize ( notAtomic++
) действительно короткая. Поэтому стоимость синхронизации действительно важна по сравнению с операцией.
Помимо атомарности AtomicInteger может использоваться как изменяемая версия, Integer
например, в Map
качестве значений.
AtomicInteger
в качестве ключа карты, потому что он использует equals()
реализацию по умолчанию , которая почти наверняка не соответствует ожидаемой семантике при использовании в карте.
Например, у меня есть библиотека, которая генерирует экземпляры некоторого класса. Каждый из этих экземпляров должен иметь уникальный целочисленный идентификатор, поскольку эти экземпляры представляют команды, отправляемые на сервер, и каждая команда должна иметь уникальный идентификатор. Поскольку нескольким потокам разрешено отправлять команды одновременно, я использую AtomicInteger для генерации этих идентификаторов. Альтернативный подход заключается в использовании некоторого типа блокировки и обычного целого числа, но это и медленнее, и менее элегантно.
Как сказал Габузо, иногда я использую AtomicIntegers, когда хочу передать int по ссылке. Это встроенный класс, в котором есть специфичный для архитектуры код, поэтому он проще и, вероятно, более оптимизирован, чем любой MutableInteger, который я мог бы быстро кодировать. Тем не менее, это похоже на злоупотребление классом.
В Java 8 атомарные классы были расширены двумя интересными функциями:
Оба используют функцию 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 .
Я обычно использую 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 не блокирует, он должен увеличить пропускную способность, сделать больше работы. Как вы, возможно, знаете, проблема «Обедающие философы» используется, когда требуется контролируемый доступ к ресурсам, то есть нужны вилки, как процесс требует ресурсов для продолжения работы.
Простой пример для функции 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 Значение не было обновлено