Відповіді:
Є два основних напрямки використання 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()
) замість збільшення (і обробляє результат перед поверненням).
read
та write that value + 1
операціями, це виявляється, а не перезаписує старе оновлення (уникаючи проблеми "втраченого оновлення"). Це насправді особливий випадок compareAndSet
- якщо старе значення було 2
, клас насправді викликає, compareAndSet(2, 3)
- тож якщо інший потік тим часом змінив значення, метод приросту ефективно перезапускається з початку.
Абсолютний найпростіший приклад, який я можу придумати, - це наростити атомну операцію.
Зі стандартними вставками:
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++
та запам'ятовуванні (чи ні) попередньо придбати правильний набір моніторів.
Якщо ви подивитеся на методи 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
Основне використання - 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 як значення.
AtomicInteger
в якості ключа карти, тому що він використовує equals()
реалізацію за замовчуванням , що майже точно не те, що, як ви очікували, використовуватиме семантика, якщо вона буде використана на карті.
Наприклад, у мене є бібліотека, яка генерує екземпляри якогось класу. Кожен з цих екземплярів повинен мати унікальний цілий ідентифікатор, оскільки ці екземпляри представляють команди, що надсилаються серверу, і кожна команда повинна мати унікальний ідентифікатор. Оскільки декілька потоків дозволено одночасно надсилати команди, я використовую AtomicInteger для створення цих ідентифікаторів. Альтернативним підходом було б використання якогось блокування та звичайного цілого числа, але це і повільніше, і менш елегантно.
Як сказав gabuzo, іноді я використовую AtomicIntegers, коли хочу передати інт за посиланням. Це вбудований клас із специфічним для архітектури кодом, тому він простіший і, швидше за все, більш оптимізований, ніж будь-який MutableInteger, який я міг швидко кодувати. Однак, це відчуває зловживання класом.
У Java 8 атомні класи були розширені двома цікавими функціями:
Обидва використовують 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 .
Зазвичай я використовую AtomicInteger, коли мені потрібно надати ID для об'єктів, до яких можна отримати доступ або створити з декількох потоків, і я зазвичай використовую його як статичний атрибут класу, до якого я отримую доступ у конструкторі об'єктів.
Ви можете реалізувати незаблокуючі блокування за допомогою порівняння та заміни (CAS) для атомних цілих чисел або довжин. «П2» Програмне забезпечення транзакционной пам'яті документ описує це:
Ми пов'язуємо спеціальний замок для запису з кожним місцем пам'яті. У своїй найпростішій формі перетворений блокування запису - це одне слово спілок, яке використовує операцію CAS для придбання блокування та магазину для його звільнення. Оскільки потрібно лише один біт, який вказує на те, що блокування зроблено, ми використовуємо решту слова блокування, щоб містити номер версії.
Що він описує, спочатку прочитайте атомне ціле число. Розділіть це на ігнорований біт блокування та номер версії. Спроба CAS записати його як розблокований біт блокування з поточним номером версії до встановленого біта блокування і наступного номера версії. Цикл, поки вам це не вдасться, і ви - нитка, якій належить замок. Розблокуйте, встановивши номер поточної версії з очищеним бітом блокування. У статті описано використання номерів версій у блокуваннях для координації того, що потоки мають послідовний набір зчитувань під час запису.
У цій статті описано, що процесори мають апаратну підтримку для порівняння та заміни операцій, що робить дуже ефективним. Він також стверджує:
неблокуючі лічильники на основі CAS, що використовують атомні змінні, мають кращу продуктивність, ніж лічильники на основі блокування в умовах низької та помірної суперечки
Ключовим є те, що вони дозволяють одночасно отримувати доступ та змінюватись. Їх зазвичай використовують як лічильники в багатопотокових середовищах - до їх введення це повинен був бути написаний користувачем клас, який загортав різні методи в синхронізовані блоки.
Я використовував 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 не блокується, він повинен збільшити пропускну здатність, зробити більше роботи. Як ви можете знати, проблема філософів їжі застосовується тоді, коли потрібен контрольований доступ до ресурсів, тобто виделки, як процес потребує ресурсів для продовження роботи.
Простий приклад для функції сравнение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 Значення не оновлено