Практическое использование для AtomicInteger


230

Я вроде понимаю, что AtomicInteger и другие переменные Atomic допускают одновременный доступ. В каких случаях этот класс обычно используется?

Ответы:


190

Есть два основных использования 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()) вместо приращения (и обрабатывает результат перед возвратом).


1
Я думаю, что я понимаю первое использование. Это делается для того, чтобы убедиться, что счетчик был увеличен до повторного доступа к атрибуту. Верный? Не могли бы вы привести короткий пример для второго использования?
Джеймс П.

8
Ваше понимание первого использования отчасти верно - это просто гарантирует , что если другой поток модифицирует счетчик между readи write that value + 1операции, это обнаруживается , а не перезаписывать старые обновления (избегая «потерянное обновление» проблему). На самом деле это особый случай compareAndSet- если старое значение было 2, класс фактически вызывает compareAndSet(2, 3)- поэтому, если другой поток изменил значение за это время, метод приращения эффективно перезапускается с самого начала.
Анджей Дойл

3
"остаток> 0? остаток: остаток + n;" в этом выражении есть ли причина добавлять остаток к n, когда он равен 0?
sandeepkunkunuru

101

Абсолютно простейший пример, который я могу придумать, - сделать инкрементную атомарную операцию

Со стандартными целями:

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++и запоминании (или нет), чтобы заранее получить правильный набор мониторов.


2
Спасибо за это четкое объяснение. Каковы будут преимущества использования AtomicInteger над классом, где все методы синхронизированы? Будет ли последний считаться «тяжелее»?
Джеймс П.

3
С моей точки зрения, это в основном инкапсуляция, которую вы получаете с AtomicIntegers - синхронизация происходит именно на то, что вам нужно, и вы получаете описательные методы, которые есть в общедоступном API, чтобы объяснить, каков ожидаемый результат. (Кроме того, в некоторой степени вы правы, часто нужно просто синхронизировать все методы в классе, который, вероятно, слишком грубый, хотя с HotSpot, выполняющим оптимизацию блокировки и правила против преждевременной оптимизации, я считаю читабельность больше пользы, чем производительности.)
Анджей Дойл

Это очень четкое и точное объяснение, спасибо!
Akash5288,

Наконец объяснение, которое правильно прояснило это для меня.
Бенни Боттема

58

Если вы посмотрите на методы, которые есть у 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


37

Основное использование 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качестве значений.


1
Я не думаю, что хотел бы использовать AtomicIntegerв качестве ключа карты, потому что он использует equals()реализацию по умолчанию , которая почти наверняка не соответствует ожидаемой семантике при использовании в карте.
Анджей Дойл

1
@Andrzej, конечно, не как ключ, который должен быть неизменным, а как значение.
Габузо

@gabuzo Есть идеи, почему атомное целое число работает лучше, чем синхронизировано?
Supun Wijerathne

Сейчас тест довольно старый (более 6 лет), мне может быть интересно провести повторное тестирование с недавней JRE. Я не углубился в AtomicInteger, чтобы ответить, но, поскольку это очень специфическая задача, он будет использовать методы синхронизации, которые работают только в этом конкретном случае. Также помните, что тест является однопоточным, и проведение аналогичного теста в сильно загруженной среде может не дать такой явной победы для AtomicInteger
gabuzo

Я считаю, что его 3 мс и 5,5 мс
Sathesh

18

Например, у меня есть библиотека, которая генерирует экземпляры некоторого класса. Каждый из этих экземпляров должен иметь уникальный целочисленный идентификатор, поскольку эти экземпляры представляют команды, отправляемые на сервер, и каждая команда должна иметь уникальный идентификатор. Поскольку нескольким потокам разрешено отправлять команды одновременно, я использую AtomicInteger для генерации этих идентификаторов. Альтернативный подход заключается в использовании некоторого типа блокировки и обычного целого числа, но это и медленнее, и менее элегантно.


Спасибо, что поделились этим практическим примером. Это звучит как то, что я должен использовать, так как мне нужно иметь уникальный идентификатор для каждого файла, который я импортирую в свою программу :)
Джеймс П.

7

Как сказал Габузо, иногда я использую AtomicIntegers, когда хочу передать int по ссылке. Это встроенный класс, в котором есть специфичный для архитектуры код, поэтому он проще и, вероятно, более оптимизирован, чем любой MutableInteger, который я мог бы быстро кодировать. Тем не менее, это похоже на злоупотребление классом.


7

В 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 .


5

Я обычно использую AtomicInteger, когда мне нужно дать идентификаторы объектам, которые могут быть приняты или созданы из нескольких потоков, и я обычно использую его как статический атрибут в классе, к которому я обращаюсь в конструкторе объектов.


4

Вы можете реализовать неблокирующие блокировки, используя compareAndSwap (CAS) для атомарных целых или длинных значений. В документе «Транзакционная память программного обеспечения Tl2» описывается это:

Мы связываем специальную версионную блокировку записи с каждым местом транзакции в памяти. В простейшей форме версионная блокировка записи представляет собой спин-блокировку из одного слова, которая использует операцию CAS для получения блокировки и хранилище для ее снятия. Поскольку требуется только один бит, чтобы указать, что блокировка взята, мы используем оставшуюся часть слова блокировки для хранения номера версии.

То, что он описывает, это сначала прочитать атомное целое число. Разделите это на игнорируемый бит блокировки и номер версии. Попытайтесь в CAS записать его как бит блокировки, сброшенный с номером текущей версии, в установленный бит блокировки и следующий номер версии. Цикл, пока вы не добьетесь успеха и ваша нить владеет замком. Разблокируйте, установив номер текущей версии с очищенным битом блокировки. В статье описывается использование номеров версий в замках для координации того, что потоки имеют постоянный набор операций чтения при записи.

В этой статье описывается, что процессоры имеют аппаратную поддержку для операций сравнения и обмена, что делает их очень эффективными. Он также утверждает:

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


3

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


Понимаю. Это в случаях, когда атрибут или экземпляр действует как своего рода глобальная переменная внутри приложения. Или есть другие случаи, о которых вы можете подумать?
Джеймс П.

1

Я использовал 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 не блокирует, он должен увеличить пропускную способность, сделать больше работы. Как вы, возможно, знаете, проблема «Обедающие философы» используется, когда требуется контролируемый доступ к ресурсам, то есть нужны вилки, как процесс требует ресурсов для продолжения работы.


0

Простой пример для функции 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 Значение не было обновлено

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.