CodeGym /Kursy /JAVA 25 SELF /Muteksy i semafory: składnia i zadania

Muteksy i semafory: składnia i zadania

JAVA 25 SELF
Poziom 52 , Lekcja 3
Dostępny

1. Muteks (Mutex): co to jest i jak działa

Muteks (od angielskiego „mutual exclusion” — „wzajemne wykluczanie”) — mechanizm, który pozwala tylko jednemu wątkowi jednocześnie wykonywać sekcję krytyczną kodu. Jeśli muteks jest zablokowany (przejęty przez inny wątek), pozostałe wątki czekają, aż zostanie zwolniony.

W Javie rolę muteksa często pełni obiekt, na którym synchronizuje się kod: synchronized. Od 5 wersji Javy pojawiła się klasa ReentrantLock — bardziej jawna i elastyczna implementacja muteksa.

Schematycznie

Wyobraźmy sobie pokój z jedynym kluczem (muteksem). Aby wejść, trzeba wziąć klucz. Jeśli klucza nie ma (ktoś już go wziął), czekasz przy drzwiach. Gdy tylko klucz wróci na miejsce (muteks zostanie zwolniony), kolejna osoba może wejść.

Składnia muteksa w Javie

Za pomocą synchronized (klasyka):

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

Tutaj cała metoda increment jest chroniona muteksem — w danym momencie może ją wykonywać tylko jeden wątek.

Za pomocą ReentrantLock (bardziej elastycznie):

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(); // Zajmujemy muteks
        try {
            count++;
        } finally {
            lock.unlock(); // Koniecznie zwalniamy!
        }
    }
}

Ważne! Zawsze zwalniaj muteks w bloku finally, w przeciwnym razie możesz dostać „wieczną blokadę” (deadlock) — i program się zawiesi.

Kiedy potrzebny jest muteks?

Muteks jest potrzebny, gdy do zasobu powinien jednocześnie podchodzić tylko jeden wątek. Może to być zmienna, plik lub baza danych. Szczególnie ważne jest użycie muteksa, jeśli praca z zasobem nie jest atomowa: nawet proste count++ w rzeczywistości składa się z trzech kroków — odczytać wartość, zwiększyć i zapisać z powrotem. Bez muteksa kilka wątków może wtrącić się między krokami i wywołać wyścig danych.

2. Semafor (Semaphore): po co jest i jak działa

Semafor — „regulator”, który pozwala jednocześnie pracować z zasobem kilku wątkom, ale nie więcej niż zadana liczba. Jeśli limit jest wyczerpany, pozostałe wątki czekają na swoją kolej.

Analogicznie: parking na 3 samochody. Jeśli wszystkie miejsca są zajęte, nowi przybysze czekają, aż ktoś odjedzie.

Składnia semafora w Javie

Do tego służy klasa Semaphore z pakietu java.util.concurrent:

import java.util.concurrent.Semaphore;

public class ParkingLot {
    private final Semaphore spots;

    public ParkingLot(int places) {
        this.spots = new Semaphore(places);
    }

    public void parkCar(String car) throws InterruptedException {
        spots.acquire(); // Próbujemy zająć miejsce (jeśli nie ma — czekamy)
        try {
            System.out.println(car + " zaparkował.");
            Thread.sleep(1000); // Samochód stoi na parkingu
        } finally {
            spots.release(); // Zwalniamy miejsce
            System.out.println(car + " odjechał.");
        }
    }
}

Użycie:

ParkingLot parking = new ParkingLot(3);

for (int i = 1; i <= 5; i++) {
    final String car = "Samochód " + i;
    new Thread(() -> {
        try {
            parking.parkCar(car);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

Wynik: jednocześnie na parkingu nie będzie więcej niż trzy samochody — pozostałe czekają.

Jak działa semafor?

  • Przy tworzeniu semafora ustala się liczbę „pozwoleń” (permits).
  • Metoda acquire() próbuje pobrać pozwolenie: jeśli jest wolne — wątek przechodzi, jeśli nie — czeka.
  • Metoda release() oddaje pozwolenie z powrotem.
  • Semafor z jednym pozwoleniem zachowuje się prawie jak muteks, ale bez „właściciela”.

3. Muteks i semafor: jaka jest różnica?

Cecha Muteks (Mutex) Semafor (Semaphore)
Liczba wątków Tylko jeden Kilka (ograniczona liczba)
Zastosowanie Ochrona zasobu Ograniczenie dostępu (np. pula)
API w Javie
synchronized, Lock
Semaphore
Zarządzanie Zwykle „właściciel” Może zwalniać dowolny wątek
Typowy scenariusz Wspólny licznik, obiekt Pula połączeń, parking, limit

- Muteks — dla przypadków, gdy potrzebny jest dostęp wyłączny.
- Semafor — gdy można dopuścić kilka wątków naraz, ale nie wszystkie.

Analogicznie: muteks — toaleta z jedną kabiną; semafor — toaleta z trzema kabinami.

4. Praktyczne przykłady zadań

Przykład 1: Muteks do ochrony sekcji krytycznej

Załóżmy, że mamy wspólny bank i kilka wątków przelewa pieniądze między kontami. Operacje muszą być atomowe.

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

public class BankAccount {
    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccount(int initial) {
        this.balance = initial;
    }

    public void deposit(int amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }

    public void withdraw(int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();
        }
    }

    public int getBalance() {
        return balance;
    }
}

Tutaj wszelkie operacje na saldzie są chronione muteksem, aby nie wystąpił race condition.

Przykład 2: Semafor do ograniczania dostępu

Serwer może jednocześnie obsługiwać tylko 2 klientów (np. z powodu licencji).

import java.util.concurrent.Semaphore;

public class Server {
    private final Semaphore connections = new Semaphore(2);

    public void handleRequest(String client) throws InterruptedException {
        connections.acquire();
        try {
            System.out.println(client + " połączył się z serwerem.");
            Thread.sleep(2000); // Symulacja przetwarzania żądania
        } finally {
            connections.release();
            System.out.println(client + " rozłączył się.");
        }
    }
}

Użycie:

Server server = new Server();

for (int i = 1; i <= 5; i++) {
    final String client = "Klient " + i;
    new Thread(() -> {
        try {
            server.handleRequest(client);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

Wynik: jednocześnie serwer obsługuje nie więcej niż dwóch klientów.

5. Cechy i niuanse użycia

Muteks: zawsze zwalniaj!
Bardzo ważne jest, aby nie zapominać o wywołaniu unlock() (lub wyjściu z bloku synchronized) nawet przy wyjątkach. Używaj try-finally:

lock.lock();
try {
    // sekcja krytyczna
} finally {
    lock.unlock();
}

Jeśli o tym zapomnieć — można uzyskać „wieczną blokadę”; inne wątki będą czekać w nieskończoność.

Semafor: czy można zwolnić cudzy wątek?
W przeciwieństwie do muteksa, release() może wywołać dowolny wątek, nawet taki, który nie robił acquire(). To czasem wygodne, ale łatwo o błąd — zachowuj dyscyplinę.

Semaphore z jednym pozwoleniem = muteks?
Prawie. Ale semafor nie ma pojęcia „właściciela”: każde zwolnienie zwiększa licznik pozwoleń. W muteksie zwalniać powinien ten, kto go przejął.

Nie myl semafora z pulą
Semafor to nie pula obiektów, a jedynie „licznik pozwoleń”. Często używa się go do implementacji pul (np. puli połączeń do bazy danych), ale sam w sobie niczego nie przechowuje.

6. Typowe błędy podczas pracy z muteksami i semaforami

Błąd nr 1: Zapomniano wywołać unlock/release. Jeśli przejęliście muteks lub semafor, ale nie wywołaliście unlock() lub release(), inne wątki mogą zawisnąć na zawsze. Zawsze używaj try-finally, aby zagwarantować zwolnienie blokady nawet przy wyjątkach.

Błąd nr 2: Synchronizacja na niewłaściwym obiekcie. Jeśli synchronizujesz się na zmiennej, która nie jest wspólna dla wszystkich wątków (np. na zmiennej lokalnej lub na literałach łańcuchowych), synchronizacja nie zadziała.

Błąd nr 3: Podwójne zwolnienie. W przypadku semafora: jeśli wywołasz release() więcej razy, niż było acquire(), liczba pozwoleń wzrośnie ponad limit. Pilnuj bilansu!

Błąd nr 4: Używanie semafora zamiast muteksa (lub odwrotnie). Jeśli potrzebny jest dostęp wyłączny, użyj muteksa (synchronized lub Lock). Jeśli trzeba ograniczyć liczbę jednocześnie działających wątków — użyj Semaphore.

Błąd nr 5: Długie przetrzymywanie blokady. Im dłużej wątek trzyma muteks lub semafor, tym dłużej inni czekają. Minimalizuj czas pracy w sekcji krytycznej.

1
Zadanie
JAVA 25 SELF, poziom 52, lekcja 3
Niedostępne
Monitorowanie zdarzeń krytycznych 🚨
Monitorowanie zdarzeń krytycznych 🚨
1
Zadanie
JAVA 25 SELF, poziom 52, lekcja 3
Niedostępne
Zarządzanie dostępem do pasów startowo‑lądowych ✈️
Zarządzanie dostępem do pasów startowo‑lądowych ✈️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION