Praktyczne zastosowania dla AtomicInteger

228

Rozumiem, że AtomicInteger i inne zmienne Atomic pozwalają na równoczesny dostęp. W jakich przypadkach ta klasa jest zwykle używana?

James P.
źródło

Odpowiedzi:

189

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

axtavt
źródło
1
Myślę, że rozumiem pierwsze użycie. Ma to na celu upewnienie się, że licznik został zwiększony przed ponownym dostępem do atrybutu. Poprawny? Czy możesz podać krótki przykład drugiego użycia?
James P.
8
Twoje zrozumienie pierwszego użycia jest w pewnym sensie prawdziwe - po prostu zapewnia, że ​​jeśli inny wątek zmodyfikuje licznik między operacjami readi 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.
Andrzej Doyle
3
„pozostała> 0? pozostała: pozostała + n;” czy w tym wyrażeniu istnieje powód, aby dodać resztę do n, gdy wynosi 0?
sandeepkunkunuru
100

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 AtomicIntegersdo 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 intsi odpowiednie synchronizeddeklaracje, piękno AtomicIntegerpolega 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 intwartoś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.

Andrzej Doyle
źródło
2
Dziękuję za to jasne wyjaśnienie. Jakie byłyby zalety używania AtomicInteger nad klasą, w której wszystkie metody są zsynchronizowane? Czy to drugie byłoby uważane za „cięższe”?
James P.
3
Z mojego punktu widzenia jest to głównie enkapsulacja uzyskiwana za pomocą AtomicIntegers - synchronizacja odbywa się dokładnie na tym, czego potrzebujesz, i otrzymujesz opisowe metody, które są w publicznym interfejsie API, aby wyjaśnić, jaki jest zamierzony wynik. (Poza tym do pewnego stopnia masz rację, często kończy się to po prostu synchronizacją wszystkich metod w klasie, która jest prawdopodobnie zbyt gruboziarnista, chociaż przy HotSpot przeprowadzającym optymalizacje blokady i zasady przedwczesnej optymalizacji uważam, że czytelność jest więcej korzyści niż wydajności.)
Andrzej Doyle
To jest bardzo jasne i precyzyjne wyjaśnienie, dzięki !!
Akash5288
Wreszcie wyjaśnienie, które właściwie dla mnie wyjaśniło.
Benny Bottema
58

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:
++iis i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iisx = i.get()

Istnieją również inne metody wygody, takie jak compareAndSetlubaddAndGet

Władca
źródło
37

Podstawowym zastosowaniem AtomicIntegerjest, 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 intjest już atomowe, ale AtomicIntegerobejmuje wiele operacji, które nie są atomowe int.

Najprostsze to getAndXXXlub 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. compareAndSetjest bardzo przydatny do implementacji semaforów, zamków, zatrzasków itp.

Używanie AtomicIntegerjest 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 Integerna przykład Mapws jako wartości.

gabuzo
źródło
1
Nie sądzę, żebym chciał użyć go AtomicIntegerjako 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.
Andrzej Doyle
1
@ Andrzej pewnie, nie jako klucz, który musi być niezmienny, ale wartość.
gabuzo
@gabuzo Wiesz, dlaczego liczba całkowita atomowa działa dobrze w porównaniu do synchronizacji?
Supun Wijerathne,
Test jest już dość stary (ponad 6 lat). Ciekawe może być ponowne przetestowanie go w najnowszym środowisku JRE. Nie poszedłem wystarczająco głęboko w AtomicInteger, aby odpowiedzieć, ale ponieważ jest to bardzo specyficzne zadanie, użyje technik synchronizacji, które działają tylko w tym konkretnym przypadku. Pamiętaj też, że test jest monotekstowy i wykonanie podobnego testu w mocno obciążonym środowisku może nie dać tak wyraźnego zwycięstwa
AtomicInteger
Wierzę, że to 3 ms i 5,5 ms
Sathesh
17

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.

Siergiej Tachenov
źródło
Dziękujemy za podzielenie się tym praktycznym przykładem. To brzmi jak coś, czego powinienem użyć, ponieważ muszę mieć unikalny identyfikator dla każdego pliku importowanego do mojego programu :)
James P.
7

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.

David Ehrmann
źródło
7

W Javie 8 klasy atomowe zostały rozszerzone o dwie interesujące funkcje:

  • int getAndUpdate (funkcja aktualizacji IntUnaryOperator)
  • int updateAndGet (IntUnaryOperator updateFunction)

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 .

pwojnowski
źródło
5

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.

DguezTorresEmmanuel
źródło
4

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

simbo1905
źródło
3

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.

Michael Berry
źródło
Widzę. Czy dzieje się tak w przypadkach, gdy atrybut lub instancja działa jako rodzaj zmiennej globalnej w aplikacji. A może są inne przypadki, o których możesz pomyśleć?
James P.
1

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.

Rodrigo Gomez
źródło
0

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

M Shafaei N.
źródło