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


228

Я якось розумію, що AtomicInteger та інші змінні Atomic дозволяють одночасно мати доступ. У яких випадках зазвичай використовується цей клас?

Відповіді:


189

Є два основних напрямки використання 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

100

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

Зі стандартними вставками:

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 все ще є входом, використовуваним для обчислення, інакше почніть заново - але приклади підрахунку дуже корисні, і я часто буду використовувати AtomicIntegersдля підрахунку та унікальних генераторів VM, якщо є якісь підказки щодо залучення декількох потоків, тому що з ними так просто працювати, я майже вважаю це передчасною оптимізацією для використання простого використання ints.

Хоча ви майже завжди можете домагатися одних і тих же гарантій синхронізації з intsвідповідними synchronizedдеклараціями, краса цього AtomicIntegerполягає в тому, що безпека потоку вбудована в сам об’єкт, а не вам потрібно турбуватися про можливі переплетення та проведені монітори кожного методу що трапляється для доступу до intзначення. Набагато складніше випадково порушити безпеку потоку під час дзвінка, getAndIncrement()ніж при поверненні i++та запам'ятовуванні (чи ні) попередньо придбати правильний набір моніторів.


2
Дякую за це чітке пояснення. Які б були переваги використання AtomicInteger над класом, де всі методи синхронізовані? Чи вважатиме останнє "важчим"?
Джеймс П.

3
З моєї точки зору, це головним чином інкапсуляція, яку ви отримуєте з AtomicIntegers - синхронізація відбувається саме в тому, що вам потрібно, і ви отримуєте описові методи, які знаходяться в загальнодоступному API, щоб пояснити, який саме призначений результат. (Плюс до певної міри ви праві, часто в кінцевому підсумку можна просто синхронізувати всі методи в класі, який, ймовірно, занадто грубозернистий, хоча з HotSpot, що виконує оптимізацію блокування та правила проти передчасної оптимізації, я вважаю, що читабельність - це більша вигода, ніж продуктивність.)
Анджей Дойл

Це дуже чітке і точне пояснення, дякую !!
Akash5288

Нарешті пояснення, яке належним чином очистило це для мене.
Бенні Боттема

58

Якщо ви подивитеся на методи 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++це 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 секунд. Проблема тут полягає в тому, що операція по синхронізації (notAtomic++ ) дійсно коротка. Тож вартість синхронізації дійсно важлива порівняно з операцією.

Крім атомності AtomicInteger можна використовувати як змінну версію, Integerнаприклад, в Maps як значення.


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

1
@Andrzej впевнений, не як ключ, який потрібно не змінювати, а значення.
габузо

@gabuzo Будь-яка ідея, чому атомне ціле число працює над синхронізованим?
Supun Wijerathne

Зараз випробування досить старі (більше 6 років), мені може бути цікавим повторно протестувати з останнім JRE. Я не пішов досить глибоко в AtomicInteger, щоб відповісти, але оскільки це дуже специфічне завдання, він використовуватиме методи синхронізації, які працюють лише в цьому конкретному випадку. Також майте на увазі, що тест є монопоточним і проведення подібного тесту у важко завантаженому середовищі може не дати такої явної перемоги AtomicInteger
габузо

Я вважаю, що його 3 мс та 5,5 мс
Sathesh

17

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


Дякуємо, що поділилися цим практичним прикладом. Це звучить як щось, що я повинен використовувати, оскільки мені потрібно мати унікальний ідентифікатор для кожного файлу, який я імпортую в свою програму :)
James P.

7

Як сказав gabuzo, іноді я використовую AtomicIntegers, коли хочу передати інт за посиланням. Це вбудований клас із специфічним для архітектури кодом, тому він простіший і, швидше за все, більш оптимізований, ніж будь-який MutableInteger, який я міг швидко кодувати. Однак, це відчуває зловживання класом.


7

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

  • int getAndUpdate (оновлення IntUnaryOperatorFunction)
  • 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 .


5

Зазвичай я використовую AtomicInteger, коли мені потрібно надати ID для об'єктів, до яких можна отримати доступ або створити з декількох потоків, і я зазвичай використовую його як статичний атрибут класу, до якого я отримую доступ у конструкторі об'єктів.


4

Ви можете реалізувати незаблокуючі блокування за допомогою порівняння та заміни (CAS) для атомних цілих чисел або довжин. «П2» Програмне забезпечення транзакционной пам'яті документ описує це:

Ми пов'язуємо спеціальний замок для запису з кожним місцем пам'яті. У своїй найпростішій формі перетворений блокування запису - це одне слово спілок, яке використовує операцію 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

Простий приклад для функції сравнениеAndSet ():

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.