CodeGym /Kursy /JAVA 25 SELF /Analiza typowych błędów synchronizacji

Analiza typowych błędów synchronizacji

JAVA 25 SELF
Poziom 52 , Lekcja 4
Dostępny

1. Zapomniany unlock/release: pułapka dla nieuważnych

Jednym z najbardziej podstępnych błędów przy użyciu nowoczesnych narzędzi synchronizacji, takich jak ReentrantLock lub Semaphore, jest zapomnienie wywołania unlock() albo release(). Jeśli nie zwolnisz blokady, inne wątki będą czekały na jej zwolnienie... w nieskończoność. Program się zawiesi, a ty będziesz długo patrzeć w ekran, próbując zrozumieć, dlaczego nic się nie dzieje.

Rozważmy przykład z ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        // Ups! Zapomnieliśmy o unlock() — teraz wszyscy się zawieszą!
        count++;
    }
}

Wszystko wygląda niewinnie, ale jeśli wywołać increment() kilka razy z różnych wątków, po pierwszym wywołaniu pozostałe wątki będą czekać na zwolnienie blokady bez końca.

Aby uniknąć tej sytuacji, użyj konstrukcji try-finally:

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

Teraz, nawet jeśli w środku metody wystąpi wyjątek, blokada zostanie na pewno zwolniona.

To tak, jakby ktoś zajął toaletę (zamknął się od środka), a potem zapomniał otworzyć drzwi i wyszedł przez okno. Reszta będzie czekać, aż ta osoba wyjdzie... Nie róbcie tak!

2. Synchronizacja na niewłaściwym obiekcie: „Och, zawiesiłem zamek nie tam, gdzie trzeba!”

W Javie słowo kluczowe synchronized może blokować dostęp do określonego obiektu. Jeśli jednak wybierzesz niewłaściwy obiekt do blokowania, synchronizacja nie zadziała tak, jak oczekujesz.

Błąd nr 1: synchronizacja na zmiennej lokalnej

public void doSomething() {
    Object lock = new Object();
    synchronized (lock) {
        // Za każdym razem nowy obiekt — żadnej synchronizacji!
        // Wątki nie czekają na siebie.
        // Sekcja krytyczna nie jest chroniona!
    }
}

Tutaj każdy wątek tworzy własny obiekt lock. W rezultacie żadna realna blokada nie zachodzi — wątki wchodzą do sekcji krytycznej jednocześnie.

Poprawnie:

private final Object lock = new Object();

public void doSomething() {
    synchronized (lock) {
        // Teraz wszystkie wątki używają tego samego obiektu lock
        // i rzeczywiście czekają na siebie.
    }
}

Błąd nr 2: synchronizacja na literałach łańcuchowych

public void doSomething() {
    synchronized ("lock") {
        // Literały łańcuchowe są internowane: różne części programu mogą
        // przypadkowo synchronizować się na tym samym łańcuchu!
    }
}

Wniosek:
Synchronizuj się tylko na prywatnych obiektach utworzonych specjalnie do tego celu, które nie są używane nigdzie indziej.

3. Wzajemna blokada (deadlock): „Ty mi — ja tobie, i obaj stoimy”

Deadlock (wzajemna blokada) to klasyka gatunku. Dwa (lub więcej) wątków na przemian przechwytują różne blokady i czekają na siebie nawzajem, aż program stanie dęba.

Przykład:

public class DeadlockExample {
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            // Poczekajmy chwilę dla czystości eksperymentu
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockB) {
                // ...
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockA) {
                // ...
            }
        }
    }
}

Jeśli jeden wątek wywoła method1(), a inny — method2(), to pierwszy przechwyci lockA i będzie czekał na lockB, a drugi — na odwrót. W rezultacie oba będą czekać na siebie w nieskończoność.

Jak uniknąć?

  • Zawsze przechwytuj blokady w tej samej kolejności we wszystkich wątkach.
  • Minimalizuj liczbę blokad utrzymywanych jednocześnie.
  • Używaj narzędzi diagnostycznych (np. jstack), jeśli program się zawiesił.

Analogia:
To tak, jakby dwie osoby spotkały się w wąskim korytarzu i każda postanowiła ustąpić, ale tylko jeśli druga ustąpi najpierw. W efekcie obie stoją i czekają, aż ktoś pierwszy odpuści.

4. Nadmierna synchronizacja: „Lepiej dmuchać na zimne?” — nie zawsze!

Czasem programiści, obawiając się błędów, synchronizują wszystko jak leci. W efekcie wydajność spada, a korzyści — zero.

Przykład:

public synchronized void add(int value) {
    // Jest tu tylko jedna linijka, która nie wymaga synchronizacji!
    System.out.println("Dodano: " + value);
}

W tym przypadku synchronizacja nie jest potrzebna: wypisywanie na ekran przez System.out.println jest już bezpieczne dla wątków, a sama metoda nie korzysta ze wspólnych zasobów.

Gdzie to jest krytyczne?
Jeśli synchronizujesz metody, które są wywoływane często i nie wymagają ochrony, gwałtownie obniżasz wydajność programu. Wątki ustawiają się w kolejce, chociaż mogłyby pracować równolegle.

Dobra praktyka:
Synchronizuj tylko to, co naprawdę konieczne. Sekcja krytyczna powinna być możliwie jak najmniejsza.

5. Nieprawidłowe użycie volatile: „Jest widoczność, nie ma atomowości!”

Modyfikator volatile w Javie gwarantuje, że zmiany zmiennej będą widoczne we wszystkich wątkach. Ale on nie gwarantuje atomowości operacji.

Błąd:

private volatile int counter = 0;

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

Operacja counter++ składa się z odczytu wartości, zwiększenia i zapisania z powrotem. Jeśli dwa wątki jednocześnie wykonują ten kod, wynikowa wartość może być mniejsza od oczekiwanej.

Poprawnie:
Dla operacji atomowych używaj synchronized, AtomicInteger lub innych klas bezpiecznych dla wątków.

import java.util.concurrent.atomic.AtomicInteger;

private final AtomicInteger counter = new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

Kiedy używać volatile?
Do prostych flag (np. „zakończyć pracę”), gdy nie jest wymagana atomowość.

1
Zadanie
JAVA 25 SELF, poziom 52, lekcja 4
Niedostępne
Niezawodny system rejestrowania transakcji 💳
Niezawodny system rejestrowania transakcji 💳
1
Zadanie
JAVA 25 SELF, poziom 52, lekcja 4
Niedostępne
Centralna drukarka biurowa 🖨️
Centralna drukarka biurowa 🖨️
1
Ankieta/quiz
Synchronizacja wątków, poziom 52, lekcja 4
Niedostępny
Synchronizacja wątków
Synchronizacja wątków
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION