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

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

Ответ 1

Существует два основных способа использования AtomicInteger:

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

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

    Вот пример неблокирующего генератора случайных чисел из Брайан Гёцц Java Concurrency На практике:

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

  • int getAndUpdate (функция обновления IntUnaryOperator)
  • 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.

Ответ 8

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

Ответ 9

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

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

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

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

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

Ответ 10

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

Ответ 11

Я использовал AtomicInteger для решения проблемы "Обедающий философ".

В моем решении экземпляры AtomicInteger использовались для представления вилок, для каждого философа необходимы два. Каждый Философ обозначается как целое число от 1 до 5. Когда философ использует вилку, AtomicInteger содержит значение философа, от 1 до 5, в противном случае вилка не используется, поэтому значение AtomicInteger равно [CN00 ].

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