Практическое использование для AtomicInteger - PullRequest
205 голосов
/ 27 января 2011

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

Ответы [ 11 ]

174 голосов
/ 27 января 2011

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

94 голосов
/ 27 января 2011

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

со стандартными значениями:

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

55 голосов
/ 27 января 2011

Если вы посмотрите на методы 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 is i.set(x)
x = i is x = i.get()

Есть и другие удобные методы, такие как compareAndSet или addAndGet

35 голосов
/ 27 января 2011

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

16 голосов
/ 27 января 2011

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

7 голосов
/ 03 декабря 2015

В Java 8 атомарные классы были расширены двумя интересными функциями:

  • int getAndUpdate (IntUnaryOperator updateFunction)
  • int updateAndGet (IntUnaryOperator updateFunction)

Оба используют функцию 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.

7 голосов
/ 06 декабря 2011

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

5 голосов
/ 26 марта 2013

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

4 голосов
/ 18 сентября 2012

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

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

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

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

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

2 голосов
/ 27 января 2011

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

...