CodeGym /Kursy /JAVA 25 SELF /AtomicInteger, AtomicReference: operacje atomowe

AtomicInteger, AtomicReference: operacje atomowe

JAVA 25 SELF
Poziom 53 , Lekcja 3
Dostępny

1. Dlaczego i++ nie działa w środowisku wielowątkowym?

Zacznijmy od klasycznego zadania: mamy zmienną‑licznik, na przykład liczbę przetworzonych zgłoszeń albo pobranych plików. Chcemy, aby kilka wątków zwiększało ten licznik. Co może pójść nie tak, jeśli po prostu napiszemy i++?

Przykład: warunek wyścigu przy inkrementacji

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // Nie jest atomowe!
    }
}

Wyobraźmy sobie, że dwa wątki jednocześnie wywołują increment(). Oba wątki czytają starą wartość, oba zwiększają ją o 1 i oba zapisują… tę samą nową wartość! W rezultacie jedna inkrementacja „gubi się”. Jeśli powtarzać to wiele razy, wynikowa wartość będzie mniejsza od oczekiwanej.

Dlaczego tak się dzieje?
Operacja i++ w rzeczywistości składa się z trzech kroków:

  1. Odczytać wartość zmiennej (np. 5).
  2. Zwiększyć tę wartość o 1.
  3. Zapisać nową wartość z powrotem w pamięci.

A w środowisku wielowątkowym inny wątek może zdążyć zmienić zmienną pomiędzy tymi krokami. Efekt — „warunek wyścigu” (race condition).

Czym są operacje atomowe?

Operacja atomowa — to działanie, które albo wykonuje się w całości, albo wcale, i żaden inny wątek nie może „wtrącić się” w środek tej operacji.

W Javie istnieje zestaw klas zapewniających takie operacje dla prymitywów i referencji. Znajdują się w pakiecie java.util.concurrent.atomic. Najpopularniejsze to:

  • AtomicInteger — atomowy typ całkowity.
  • AtomicLong — atomowy long.
  • AtomicBoolean — atomowy boolean.
  • AtomicReference<T> — atomowa referencja do obiektu dowolnego typu.

2. AtomicInteger: licznik bezpieczny wątkowo

Deklaracja i podstawowe użycie

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Atomowe zwiększenie
    }

    public int get() {
        return count.get();
    }
}

Tutaj incrementAndGet() wykonuje „zwiększ i zwróć nową wartość” jako jedną niepodzielną operację. Nawet jeśli 100 wątków jednocześnie wywoła tę metodę, żadna inkrementacja się nie zgubi.

Przydatne metody:

Metoda Opis
get()
Pobierz bieżącą wartość
set(int value)
Ustaw wartość
incrementAndGet()
Zwiększ o 1 i zwróć nową wartość
getAndIncrement()
Zwróć bieżącą wartość i zwiększ o 1
addAndGet(int delta)
Zwiększ o delta i zwróć nową wartość
compareAndSet(expect, update)
Jeśli bieżąca wartość równa jest expect, ustaw update (CAS)

Przykład: wielowątkowy licznik

Załóżmy, że mamy klasę, która zlicza liczbę przetworzonych wiadomości na czacie.

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("Łącznie wiadomości: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

Wnętrze: jak działa AtomicInteger?

Wewnątrz AtomicInteger wykorzystuje specjalną instrukcję procesora — CAS (Compare-And-Swap, „porównaj i zamień”). Jest to operacja atomowa, która porównuje bieżącą wartość zmiennej z oczekiwaną i jeśli są równe — zapisuje nową wartość. Jeśli inny wątek zdążył zmienić zmienną — operacja nie jest wykonana i następuje ponowna próba.

Schemat działania:

1. Czytamy bieżącą wartość (np. 5)
2. Porównujemy z oczekiwaną (5)
3. Jeśli się zgadza — zapisujemy nową wartość (6)
4. Jeśli się nie zgadza — ponawiamy próbę

Wszystko to dzieje się bardzo szybko i bez blokad (lock‑free). Dlatego klasy atomowe są często szybsze niż synchronized, zwłaszcza przy dużej liczbie wątków.

3. AtomicReference: atomowa referencja na obiekt

AtomicReference<T> to uniwersalny atomowy kontener dla dowolnego obiektu. Pozwala bezpiecznie zmieniać referencję do obiektu z wielu wątków.

Przykład: wątkowo‑bezpieczna aktualizacja referencji

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

Zastosowanie compareAndSet

Najciekawsza operacja to compareAndSet(expected, newValue). Pozwala zaktualizować wartość tylko wtedy, gdy nie zmieniła się od czasu ostatniego odczytu.

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("Aktualizacja powiodła się!");
    } else {
        System.out.println("Ktoś już zmienił wartość, spróbuj ponownie.");
    }
}

To podstawa algorytmów nieblokujących: od kolejek i stosów po cache, gdzie ważne jest unikanie zbędnych blokad.

4. Przykłady użycia w aplikacji

Przykład 1: wielowątkowy licznik wiadomości

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... przetwarzanie wiadomości ...
        int count = messageCount.incrementAndGet();
        System.out.println("Nowa wiadomość: " + message + ". Łącznie wiadomości: " + count);
    }
}

Przykład 2: bezpieczna aktualizacja referencji do ostatniej wiadomości

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... przetwarzanie ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

Jeśli trzeba aktualizować referencję tylko wtedy, gdy ostatnia wiadomość nie uległa zmianie (aby uniknąć „utraty” przy jednoczesnych aktualizacjach), użyj compareAndSet.

5. Ograniczenia i pułapki

Kiedy klasy atomowe nie są panaceum?

Zmienne atomowe świetnie nadają się do prostych operacji: inkrementacji, ustawiania wartości, sprawdzania i zamiany. Jednak gdy trzeba zaktualizować kilka zmiennych jednocześnie, atomowość nie jest już gwarantowana. Na przykład, jeśli masz dwa liczniki i chcesz zwiększyć oba jako jedną operację — potrzebny jest tutaj synchronized lub inny mechanizm synchronizacji.

Przykład nieprawidłowego użycia

// NIE jest atomowe!
if (ref.get() == null) {
    ref.set("Hello");
}

Pomiędzy get() a set(...) inny wątek może zmienić wartość i warunek będzie już nieprawdziwy. W takich przypadkach używaj compareAndSet.

Klasy atomowe ≠ obiekty bezpieczne wątkowo

Jeśli obiekt, na który wskazuje AtomicReference, sam nie jest bezpieczny wątkowo, to podmiana referencji będzie atomowa, ale zmiana pól obiektu — już nie. Na przykład, jeśli w AtomicReference<List<String>> przechowujesz zwykły ArrayList, to samą listę nie uczyni to thread‑safe.

6. Zaawansowane klasy atomowe

W pakiecie java.util.concurrent.atomic są też inne przydatne klasy:

  • AtomicLong, AtomicBoolean — dla long i boolean.
  • AtomicIntegerArray, AtomicReferenceArray — operacje atomowe na tablicach.
  • LongAdder, LongAccumulator — dla wysoko obciążonych liczników.

LongAdder i LongAccumulator

Jeśli masz bardzo dużo wątków i zwykły AtomicInteger staje się „wąskim gardłem” (wszystkie wątki konkurują o jedną zmienną), użyj LongAdder. Dzieli on licznik na kilka wewnętrznych komórek i sumuje je przy odczycie wartości, co daje zysk przy wysokiej konkurencji.

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. Typowe błędy podczas pracy ze zmiennymi atomowymi

Błąd №1: Oczekiwanie atomowości złożonych operacji.
Jeśli trzeba wykonać kilka działań na wartości, klasy atomowe nie pomogą — między krokami inny wątek może zmienić dane. Dla operacji złożonych używaj compareAndSet lub synchronizacji.

Błąd №2: Ignorowanie thread‑safety obiektów zagnieżdżonych.
Jeśli w AtomicReference leży zwykły obiekt, jego metody i pola nie stają się bezpieczne wątkowo. Atomowa jest tylko podmiana referencji.

Błąd №3: Używanie klas atomowych bez potrzeby.
W kodzie jednowątkowym typy atomowe są zbędne i nieco wolniejsze od zwykłych zmiennych z powodu dodatkowych sprawdzeń.

Błąd №4: Przedwczesna optymalizacja.
Czasami prościej i pewniej jest użyć synchronized, zwłaszcza gdy logika jest złożona i dotyka kilku zmiennych naraz. Budowanie rozwiązań lock‑free nie zawsze jest uzasadnione.

Błąd №5: Zapomniana kwestia problemu ABA.
Rzadki, ale istotny przypadek: wartość zmienia się z A na B i z powrotem na A — compareAndSet „myśli”, że nic się nie zmieniło. W takich scenariuszach używaj specjalnych klas, takich jak AtomicStampedReference (lub AtomicMarkableReference).

1
Zadanie
JAVA 25 SELF, poziom 53, lekcja 3
Niedostępne
Zliczanie uczestników na megawydarzeniu: Wątkowo bezpieczny licznik 🏟️
Zliczanie uczestników na megawydarzeniu: Wątkowo bezpieczny licznik 🏟️
1
Zadanie
JAVA 25 SELF, poziom 53, lekcja 3
Niedostępne
Aktualizacja bieżącego lidera gildii: Bezpieczna aktualizacja obiektu za pomocą AtomicReference 👑
Aktualizacja bieżącego lidera gildii: Bezpieczna aktualizacja obiektu za pomocą AtomicReference 👑
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION