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

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

10 ответов


есть два основных применения AtomicInteger:

  • как атомный счетчик (incrementAndGet() и т. д.), которые могут использоваться многими потоками одновременно

  • как примитив, который поддерживает сравнить-и-поменять инструкции (compareAndSet()) для реализации неблокирующих алгоритмов.

    вот пример неблокирующего генератора случайных чисел из Брайан Göetz на практике:

    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;
                }
            }
        }
        ...
    }
    

    As вы можете видеть, что он в основном работает почти так же, как incrementAndGet(), но выполняет произвольный расчет (calculateNext()) вместо инкремента (и обрабатывает результат перед возвращением).


самый простой пример, который я могу придумать, - это сделать приращение атомной операции.

со стандартными ints:

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() как тип оптимистическая блокировка-получить текущее значение, вычислить результат на основе этого, установить этот результат iff value по-прежнему используется для вычисления, иначе начните снова - но примеры подсчета очень полезны, и я часто буду использовать AtomicIntegers для подсчета и уникальных генераторов VM-wide, если есть какой-либо намек на участие нескольких потоков, потому что с ними так легко работать, я бы почти счел преждевременной оптимизацией использовать plain ints.

пока вы можете почти всегда достигайте одинаковых гарантий синхронизации с ints и соответствующего synchronized заявления, красота AtomicInteger заключается в том, что потокобезопасность встроена в сам фактический объект, а не вам нужно беспокоиться о возможных перемежениях и мониторах каждого метода, который происходит для доступа к int значение. Гораздо сложнее случайно нарушить threadsafety при вызове getAndIncrement() чем при возврате i++ и запоминание (или нет), чтобы приобрести правильный набор мониторов заранее.


если вы посмотрите на методы AtomicInteger, вы заметите, что они, как правило, соответствуют общим операциям на ints. Например:

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++ is i.getAndIncrement()
--i is 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как ценности.


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


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


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


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


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

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

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

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

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


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