Odpowiedzi:
Istnieją dwa główne zastosowania AtomicInteger
:
Jako licznik atomowy ( incrementAndGet()
itp.), Który może być używany jednocześnie przez wiele wątków
Jako prymityw, który obsługuje instrukcje porównywania i zamiany ( compareAndSet()
) do implementacji algorytmów nieblokujących.
Oto przykład nieblokującego generatora liczb losowych z Java Concordrency Briana Göetza w praktyce :
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;
}
}
}
...
}
Jak widać, w zasadzie działa prawie tak samo jak incrementAndGet()
, ale wykonuje dowolne obliczenia ( calculateNext()
) zamiast inkrementacji (i przetwarza wynik przed powrotem).
read
i write that value + 1
, zostanie to wykryte, a nie zastąpi starą aktualizację (unikając problemu „utraconej aktualizacji”). Jest to w rzeczywistości szczególny przypadek compareAndSet
- jeśli stara wartość była 2
, klasa faktycznie wywołuje compareAndSet(2, 3)
- więc jeśli inny wątek zmodyfikował wartość w międzyczasie, metoda przyrostowa zostanie zrestartowana od początku.
Absolutnie najprostszym przykładem, jaki mogę wymyślić, jest zwiększenie operacji atomowej.
Ze standardowymi ints:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
Z AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Ten ostatni jest bardzo prostym sposobem na wykonanie prostych efektów mutacji (zwłaszcza zliczanie lub indeksowanie unikalne), bez konieczności uciekania się do synchronizacji całego dostępu.
Bardziej złożoną logikę bez synchronizacji można zastosować compareAndSet()
jako rodzaj optymistycznego blokowania - uzyskaj bieżącą wartość, oblicz wynik na podstawie tego, ustaw ten wynik wartość iff jest nadal danymi wejściowymi używanymi do obliczeń, w przeciwnym razie zacznij od nowa - ale przykłady zliczania są bardzo przydatne i często używam AtomicIntegers
do zliczania i unikatowych generatorów maszyn wirtualnych, jeśli istnieje jakakolwiek wskazówka na wiele wątków, ponieważ są one tak łatwe w obsłudze, że prawie uważam za przedwczesną optymalizację, aby używać zwykłego ints
.
Chociaż prawie zawsze można osiągnąć te same gwarancje synchronizacji ints
i odpowiednie synchronized
deklaracje, piękno AtomicInteger
polega na tym, że bezpieczeństwo wątków jest wbudowane w sam obiekt, zamiast martwić się możliwymi przeplotami i monitorami każdej metody tak się dzieje, aby uzyskać dostęp do int
wartości. Znacznie trudniej jest przypadkowo naruszyć bezpieczeństwo wątków podczas połączenia getAndIncrement()
niż podczas powrotu i++
i zapamiętywania (lub nie) uzyskania wcześniej odpowiedniego zestawu monitorów.
Jeśli spojrzysz na metody, które ma AtomicInteger, zauważysz, że zwykle odpowiadają one typowym operacjom na ints. Na przykład:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
to wersja tego wątku:
static int i;
// Later, in a thread
int current = ++i;
Metody map w następujący sposób:
++i
is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
isx = i.get()
Istnieją również inne metody wygody, takie jak compareAndSet
lubaddAndGet
Podstawowym zastosowaniem AtomicInteger
jest, gdy jesteś w kontekście wielowątkowym i musisz wykonywać bezpieczne operacje wątkowe na liczbie całkowitej bez użycia synchronized
. Przypisywanie i wyszukiwanie typu pierwotnego int
jest już atomowe, ale AtomicInteger
obejmuje wiele operacji, które nie są atomowe int
.
Najprostsze to getAndXXX
lub xXXAndGet
. Na przykład getAndIncrement()
jest atomowym odpowiednikiem, i++
który nie jest atomowy, ponieważ w rzeczywistości jest skrótem do trzech operacji: pobierania, dodawania i przypisywania. compareAndSet
jest bardzo przydatny do implementacji semaforów, zamków, zatrzasków itp.
Używanie AtomicInteger
jest szybsze i bardziej czytelne niż wykonywanie tego samego przy użyciu synchronizacji.
Prosty test:
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));
}
Na moim komputerze z Javą 1.6 test atomowy uruchamia się w 3 sekundy, a synchronizowany w około 5,5 sekundy. Problem polega na tym, że operacja synchronizacji ( notAtomic++
) jest naprawdę krótka. Koszt synchronizacji jest więc naprawdę ważny w porównaniu z operacją.
Oprócz atomowości AtomicInteger może być używany jako modyfikowalna wersja Integer
na przykład Map
ws jako wartości.
AtomicInteger
jako klucza mapy, ponieważ używa domyślnej equals()
implementacji, co prawie na pewno nie jest tym, czego można oczekiwać od semantyki, jeśli jest używana w mapie.
Na przykład mam bibliotekę, która generuje instancje niektórych klas. Każda z tych instancji musi mieć unikalny identyfikator liczby całkowitej, ponieważ instancje te reprezentują polecenia wysyłane do serwera, a każda komenda musi mieć unikalny identyfikator. Ponieważ wiele wątków może jednocześnie wysyłać polecenia, do generowania tych identyfikatorów używam AtomicInteger. Alternatywnym podejściem byłoby użycie pewnego rodzaju blokady i zwykłej liczby całkowitej, ale jest to zarówno wolniejsze, jak i mniej eleganckie.
Jak powiedział gabuzo, czasami używam AtomicIntegers, gdy chcę przekazać int przez referencję. Jest to klasa wbudowana, która ma kod specyficzny dla architektury, więc jest łatwiejsza i prawdopodobnie bardziej zoptymalizowana niż jakikolwiek MutableInteger, który mógłbym szybko zakodować. To powiedziawszy, to jest nadużycie klasy.
W Javie 8 klasy atomowe zostały rozszerzone o dwie interesujące funkcje:
Obaj używają funkcji updateFunction, aby przeprowadzić aktualizację wartości atomowej. Różnica polega na tym, że pierwsza zwraca starą wartość, a druga zwraca nową wartość. Funkcja updateFunction może być zaimplementowana w celu wykonywania bardziej złożonych operacji „porównywania i ustawiania” niż standardowa. Na przykład może sprawdzić, czy licznik atomowy nie spadnie poniżej zera, normalnie wymagałoby synchronizacji, a tutaj kod jest bez blokady:
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;
}
}
Kod pochodzi z Java Atomic Example .
Zwykle używam AtomicInteger, gdy muszę podać identyfikatory obiektom, które można uzyskać lub utworzyć z wielu wątków, i zwykle używam go jako statycznego atrybutu klasy, do której uzyskuję dostęp w konstruktorze obiektów.
Możesz zaimplementować blokady nieblokujące za pomocą CompareAndSwap (CAS) na liczbach całkowitych lub długich atomach. „TL2” Oprogramowanie transakcyjna pamięci artykule opisano poniżej:
Specjalną wersjonowaną blokadę zapisu kojarzymy z każdą transakcyjną lokalizacją pamięci. W najprostszej formie wersjonowana blokada zapisu jest blokadą pojedynczego słowa, która wykorzystuje operację CAS w celu uzyskania blokady i magazynu w celu jej zwolnienia. Ponieważ wystarczy jeden bit, aby wskazać, że blokada jest zajęta, używamy reszty słowa blokującego do przechowywania numeru wersji.
To, co opisuje, to najpierw przeczytaj liczbę całkowitą atomową. Podziel to na zignorowany bit blokady i numer wersji. Spróbuj napisać CAS, gdy bit blokady zostanie wyczyszczony z bieżącym numerem wersji do zestawu bitów blokady i kolejnym numerem wersji. Pętla, dopóki nie odniesiesz sukcesu, a twój wątek jest właścicielem zamka. Odblokuj, ustawiając bieżący numer wersji z wyczyszczonym bitem blokady. Artykuł opisuje używanie numerów wersji w blokadach w celu skoordynowania, że wątki mają spójny zestaw odczytów podczas pisania.
W tym artykule opisano, że procesory mają sprzętową obsługę operacji porównywania i zamiany, dzięki czemu są bardzo wydajne. Twierdzi także:
nieblokujące liczniki oparte na CAS wykorzystujące zmienne atomowe mają lepszą wydajność niż liczniki oparte na blokadzie w niskiej do umiarkowanej rywalizacji
Kluczem jest to, że umożliwiają bezpieczny jednoczesny dostęp i modyfikację. Są one powszechnie używane jako liczniki w środowisku wielowątkowym - przed ich wprowadzeniem musiała to być klasa napisana przez użytkownika, która zawierała różne metody w zsynchronizowanych blokach.
Użyłem AtomicInteger, aby rozwiązać problem z filozofem restauracji.
W moim rozwiązaniu instancje AtomicInteger zostały użyte do przedstawienia widelców, potrzebne są dwa na filozofa. Każdy filozof jest identyfikowany jako liczba całkowita, od 1 do 5. Kiedy filozof używa rozwidlenia, AtomicInteger przechowuje wartość filozofa od 1 do 5, w przeciwnym razie rozwidlenie nie jest używane, więc wartość AtomicInteger wynosi -1 .
AtomicInteger pozwala następnie sprawdzić, czy widelec jest wolny, wartość == - 1, i ustawić go na właściciela widelca, jeśli jest wolny, w jednej operacji atomowej. Zobacz kod poniżej.
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);
}
}
}
Ponieważ metoda CompareAndSet nie blokuje, powinna zwiększyć przepustowość, wykonać więcej pracy. Jak zapewne wiesz, problem filozofów gastronomicznych jest wykorzystywany, gdy potrzebny jest kontrolowany dostęp do zasobów, tj. Widelce, tak jak proces potrzebuje zasobów do kontynuowania pracy.
Prosty przykład funkcji CompareAndSet ():
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");
}
}
Wydrukowano: poprzednia wartość: 0 Wartość została zaktualizowana i wynosi 6 Kolejny prosty przykład:
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");
}
}
Wydrukowano: Poprzednia wartość: 0 Wartość nie została zaktualizowana