CodeGym /Kurse /JAVA 25 SELF /Mutexe und Semaphore: Syntax und Aufgaben

Mutexe und Semaphore: Syntax und Aufgaben

JAVA 25 SELF
Level 52 , Lektion 3
Verfügbar

1. Mutex: Was ist das und wie funktioniert er

Mutex (vom Englischen „mutual exclusion“ – „gegenseitiger Ausschluss“) – ein Mechanismus, der es nur einem Thread erlaubt, gleichzeitig einen kritischen Abschnitt des Codes auszuführen. Ist der Mutex belegt (von einem anderen Thread gehalten), warten die übrigen Threads, bis er freigegeben wird.

In Java übernimmt häufig ein Objekt, auf dem der Code synchronisiert wird: synchronized, die Rolle des Mutex. Seit 5 gibt es in Java die Klasse ReentrantLock – eine explizitere und flexiblere Implementierung eines Mutex.

Schematisch

Stellen Sie sich einen Raum mit einem einzigen Schlüssel (Mutex) vor. Um einzutreten, muss man den Schlüssel nehmen. Wenn der Schlüssel nicht da ist (ihn hat bereits jemand genommen), warten Sie an der Tür. Sobald der Schlüssel zurückgelegt wird (der Mutex freigegeben ist), kann die nächste Person eintreten.

Mutex-Syntax in Java

Über synchronized (klassisch):

public class Counter {
    private int count = 0;

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

Hier ist die gesamte Methode increment durch einen Mutex geschützt – zu einem Zeitpunkt kann sie nur von einem Thread ausgeführt werden.

Über ReentrantLock (flexibler):

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(); // Mutex sperren
        try {
            count++;
        } finally {
            lock.unlock(); // Unbedingt freigeben!
        }
    }
}

Wichtig! Geben Sie den Mutex immer im Block finally frei, sonst riskieren Sie eine „ewige Blockierung“ (deadlock) – und das Programm hängt.

Wann braucht man einen Mutex?

Ein Mutex ist notwendig, wenn ein Zugriff auf eine Ressource immer nur durch genau einen Thread erfolgen soll. Das kann eine Variable, eine Datei oder eine Datenbank sein. Besonders wichtig ist der Einsatz eines Mutex, wenn die Arbeit mit der Ressource nicht atomar ist: Selbst ein einfaches count++ besteht in Wirklichkeit aus drei Schritten – Wert lesen, erhöhen und zurückschreiben. Ohne Mutex können mehrere Threads zwischen die Schritte geraten und eine Race Condition verursachen.

2. Semaphore: wozu sie dient und wie sie funktioniert

Semaphore – ein „Regler“, der mehreren Threads gleichzeitig den Zugriff auf eine Ressource erlaubt, jedoch höchstens bis zu einer vorgegebenen Anzahl. Ist das Limit erreicht, warten die übrigen Threads, bis sie an der Reihe sind.

Analogie: ein Parkplatz für 3 Autos. Wenn alle Plätze belegt sind, warten Neuankömmlinge, bis jemand wegfährt.

Semaphore-Syntax in Java

Dazu dient die Klasse Semaphore aus dem Paket 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(); // Einen Platz belegen (wenn keiner frei ist – warten)
        try {
            System.out.println(car + " hat geparkt.");
            Thread.sleep(1000); // Das Auto steht auf dem Parkplatz
        } finally {
            spots.release(); // Platz freigeben
            System.out.println(car + " ist weggefahren.");
        }
    }
}

Verwendung:

ParkingLot parking = new ParkingLot(3);

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

Ergebnis: Auf dem Parkplatz stehen nie mehr als drei Autos gleichzeitig – die anderen warten.

Wie funktioniert eine Semaphore?

  • Beim Erzeugen einer Semaphore wird die Anzahl der „Erlaubnisse“ (Permits) festgelegt.
  • Die Methode acquire() versucht, ein Permit zu nehmen: Ist eines frei – darf der Thread passieren, sonst wartet er.
  • Die Methode release() gibt ein Permit zurück.
  • Eine Semaphore mit einem Permit verhält sich fast wie ein Mutex, aber ohne „Besitzer“.

3. Mutex vs. Semaphore: Worin besteht der Unterschied?

Merkmal Mutex Semaphore
Anzahl Threads Nur einer Mehrere (begrenzte Anzahl)
Einsatz Ressourcenschutz Zugriff begrenzen (z. B. Pool)
API in Java
synchronized, Lock
Semaphore
Steuerung In der Regel besitzgebunden Freigabe durch jeden Thread möglich
Typisches Szenario Gemeinsamer Zähler, Objekt Verbindungs-Pool, Parkplatz, Limit

- Mutex – für Fälle, in denen exklusiver Zugriff nötig ist.
- Semaphore – wenn mehrere dürfen, aber nicht alle.

Analogie: Mutex – eine Toilette mit einer Kabine; Semaphore – eine Toilette mit drei Kabinen.

4. Praktische Aufgabenbeispiele

Beispiel 1: Mutex zum Schutz eines kritischen Abschnitts

Angenommen, wir haben eine gemeinsame Bank, und mehrere Threads überweisen Geld zwischen Konten. Die Operationen müssen atomar sein.

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;
    }
}

Hier sind alle Operationen am Kontostand durch einen Mutex geschützt, um eine Race Condition zu vermeiden.

Beispiel 2: Semaphore zur Zugriffsbeschränkung

Ein Server kann gleichzeitig nur 2 Clients verarbeiten (z. B. wegen der Lizenz).

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 + " hat sich mit dem Server verbunden.");
            Thread.sleep(2000); // Simulation der Anfragebearbeitung
        } finally {
            connections.release();
            System.out.println(client + " hat die Verbindung getrennt.");
        }
    }
}

Verwendung:

Server server = new Server();

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

Ergebnis: Der Server bedient gleichzeitig nicht mehr als zwei Clients.

5. Besonderheiten und Anwendungshinweise

Mutex: immer freigeben!
Es ist sehr wichtig, unlock() (oder den synchronisierten Block zu verlassen) auch bei Ausnahmen nicht zu vergessen. Verwenden Sie try-finally:

lock.lock();
try {
    // kritischer Abschnitt
} finally {
    lock.unlock();
}

Wenn man es vergisst, kann es zu einer „ewigen Blockierung“ kommen; andere Threads warten unbegrenzt.

Semaphore: Darf man für einen fremden Thread freigeben?
Im Unterschied zum Mutex kann release() von jedem Thread aufgerufen werden, sogar von einem, der kein acquire() gemacht hat. Das ist manchmal praktisch, aber fehlerträchtig – Disziplin ist wichtig.

Semaphore mit einem Permit = Mutex?
Fast. Beim Semaphore gibt es keinen „Besitzer“: Jede Freigabe erhöht den Permit-Zähler. Beim Mutex muss derjenige freigeben, der ihn gesperrt hat.

Semaphore nicht mit einem Pool verwechseln
Ein Semaphore ist kein Objektpool, sondern nur ein „Permit-Zähler“. Er wird oft zur Implementierung von Pools verwendet (z. B. ein DB-Verbindungs-Pool), speichert selbst aber nichts.

6. Häufige Fehler im Umgang mit Mutexen und Semaphoren

Fehler Nr. 1: unlock/release vergessen. Wenn Sie einen Mutex oder eine Semaphore gesperrt haben, aber unlock() oder release() nicht aufrufen, können andere Threads für immer hängen bleiben. Verwenden Sie immer try-finally, um die Freigabe der Sperre auch bei Ausnahmen zu garantieren.

Fehler Nr. 2: Synchronisation auf dem falschen Objekt. Wenn Sie auf einer Variablen synchronisieren, die nicht allen Threads gemeinsam ist (z. B. auf einer lokalen Variable oder einem String-Literal), funktioniert die Synchronisation nicht.

Fehler Nr. 3: Doppelte Freigabe. Bei einer Semaphore: Wenn release() öfter aufgerufen wird als acquire(), erhöht sich die Anzahl der Permits über das Limit hinaus. Achten Sie auf die Balance!

Fehler Nr. 4: Semaphore statt Mutex (oder umgekehrt) verwendet. Wenn exklusiver Zugriff benötigt wird, verwenden Sie einen Mutex (synchronized oder Lock). Soll die Anzahl gleichzeitig arbeitender Threads begrenzt werden – verwenden Sie Semaphore.

Fehler Nr. 5: Sperre zu lange gehalten. Je länger ein Thread einen Mutex oder eine Semaphore hält, desto länger warten andere. Minimieren Sie die Zeit im kritischen Abschnitt.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION