CodeGym /Corsi /JAVA 25 SELF /Mutex e semafori: sintassi e casi d'uso

Mutex e semafori: sintassi e casi d'uso

JAVA 25 SELF
Livello 52 , Lezione 3
Disponibile

1. Mutex (Mutex): che cos'è e come funziona

Mutex (dall’inglese «mutual exclusion» — «esclusione reciproca») — è un meccanismo che permette a un solo thread di eseguire contemporaneamente una sezione critica di codice. Se il mutex è occupato (acquisito da un altro thread), gli altri thread attendono finché non viene rilasciato.

In Java il ruolo del mutex è spesso svolto da un oggetto su cui si sincronizza il codice: synchronized. A partire dalla versione 5 di Java è stata introdotta la classe ReentrantLock — una realizzazione del mutex più esplicita e flessibile.

Schema

Immaginate una stanza con una sola chiave (mutex). Per entrare bisogna prendere la chiave. Se la chiave non c’è (qualcuno l’ha già presa), aspettate alla porta. Non appena la chiave torna al suo posto (il mutex viene rilasciato), la persona successiva può entrare.

Sintassi del mutex in Java

Tramite synchronized (classico):

public class Counter {
    private int count = 0;

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

Qui l’intero metodo increment è protetto da un mutex — solo un thread può eseguirlo in un dato momento.

Tramite ReentrantLock (più flessibile):

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(); // Acquisiamo il mutex
        try {
            count++;
        } finally {
            lock.unlock(); // Rilasciare sempre!
        }
    }
}

Importante! Rilasciate sempre il mutex nel blocco finally, altrimenti potreste ottenere un «blocco eterno» (deadlock) — e il programma si bloccherà.

Quando serve un mutex?

Un mutex è necessario quando a una risorsa deve accedere un solo thread alla volta. Può trattarsi di una variabile, di un file o di un database. È particolarmente importante usare un mutex se il lavoro con la risorsa non è atomico: anche un semplice count++ in realtà consiste di tre passaggi — leggere il valore, incrementarlo e scriverlo di nuovo. Senza mutex più thread possono interferire tra i passaggi e causare una race condition.

2. Semaforo (Semaphore): a cosa serve e come funziona

Il semaforo è un «regolatore» che consente a più thread di lavorare contemporaneamente con una risorsa, ma non oltre un numero prestabilito. Se il limite è esaurito, gli altri thread attendono il proprio turno.

Analogia: un parcheggio per 3 auto. Se tutti i posti sono occupati, i nuovi arrivati aspettano finché qualcuno non se ne va.

Sintassi del semaforo in Java

Si usa la classe Semaphore dal package 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(); // Cerchiamo di occupare un posto (se non c’è — attendiamo)
        try {
            System.out.println(car + " ha parcheggiato.");
            Thread.sleep(1000); // L'auto resta nel parcheggio
        } finally {
            spots.release(); // Liberiamo il posto
            System.out.println(car + " è andata via.");
        }
    }
}

Utilizzo:

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

Risultato: contemporaneamente nel parcheggio non ci saranno più di tre auto — le altre attendono.

Come funziona un semaforo?

  • Alla creazione del semaforo si imposta il numero di «permessi» (permits).
  • Il metodo acquire() prova a prendere un permesso: se ce n’è uno libero il thread passa, altrimenti attende.
  • Il metodo release() restituisce il permesso.
  • Un semaforo con un solo permesso si comporta quasi come un mutex, ma senza «proprietario».

3. Mutex e semaforo: qual è la differenza?

Caratteristica Mutex (Mutex) Semaforo (Semaphore)
Numero di thread Solo uno Più di uno (numero limitato)
Uso Protezione della risorsa Limitazione dell’accesso (ad es., pool)
API in Java
synchronized, Lock
Semaphore
Gestione Di solito «proprietario» Può essere rilasciato da qualsiasi thread
Scenario tipico Contatore condiviso, oggetto Pool di connessioni, parcheggio, limite

- Mutex — per i casi in cui serve accesso esclusivo.
- Semaforo — quando si possono far passare più thread, ma non tutti.

Analogia: mutex — toilette con una sola cabina; semaforo — toilette con tre cabine.

4. Esempi pratici

Esempio 1: Mutex per proteggere una sezione critica

Supponiamo di avere una banca comune e più thread trasferiscono denaro tra conti. Le operazioni devono essere atomiche.

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

Qui tutte le operazioni sul saldo sono protette da un mutex, per evitare una race condition.

Esempio 2: Semaforo per limitare l’accesso

Il server può gestire contemporaneamente solo 2 client (ad esempio a causa della licenza).

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 + " si è connesso al server.");
            Thread.sleep(2000); // Simulazione dell'elaborazione della richiesta
        } finally {
            connections.release();
            System.out.println(client + " si è disconnesso.");
        }
    }
}

Utilizzo:

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

Risultato: il server gestisce simultaneamente non più di due client.

5. Particolarità e accortezze d’uso

Mutex: rilascia sempre!
È fondamentale non dimenticare di chiamare unlock() (o uscire dal blocco sincronizzato) anche in caso di eccezioni. Usate try-finally:

lock.lock();
try {
    // sezione critica
} finally {
    lock.unlock();
}

Se lo dimenticate — potete incorrere in un «blocco eterno»; gli altri thread aspetteranno all’infinito.

Semaforo: si può rilasciare da un altro thread?
A differenza del mutex, release() può essere chiamato da qualsiasi thread, anche da quello che non ha fatto acquire(). A volte è comodo, ma è facile sbagliare — mantenete la disciplina.

Semaphore con un solo permesso = mutex?
Quasi. Ma il semaforo non ha il concetto di «proprietario»: ogni rilascio incrementa il contatore dei permessi. Nel mutex invece deve rilasciare chi lo ha acquisito.

Non confondere semaforo e pool
Un semaforo non è un pool di oggetti, ma solo un «contatore di permessi». Spesso lo si usa per implementare pool (ad esempio, un pool di connessioni al DB), ma di per sé non contiene nulla.

6. Errori tipici nell’uso di mutex e semafori

Errore n. 1: Dimenticare di chiamare unlock/release. Se avete acquisito un mutex o un semaforo ma non chiamate unlock() o release(), altri thread possono bloccarsi per sempre. Usate sempre try-finally per garantire il rilascio del lock anche in caso di eccezioni.

Errore n. 2: Sincronizzazione sull’oggetto sbagliato. Se vi sincronizzate su una variabile che non è condivisa tra tutti i thread (ad esempio, una variabile locale o un literal di stringa), la sincronizzazione non funzionerà.

Errore n. 3: Doppio rilascio. Nel caso del semaforo: se chiamate release() più volte di quante siano state le chiamate a acquire(), il numero di permessi aumenterà oltre il limite. Tenete sotto controllo il bilanciamento!

Errore n. 4: Usare un semaforo al posto di un mutex (o viceversa). Se serve accesso esclusivo, usate un mutex (synchronized o Lock). Se occorre limitare il numero di thread che operano simultaneamente — usate Semaphore.

Errore n. 5: Tenere il lock troppo a lungo. Più a lungo un thread mantiene un mutex o un semaforo, più a lungo gli altri attendono. Minimizate il tempo di lavoro all’interno della sezione critica.

1
Compito
JAVA 25 SELF, livello 52, lezione 3
Bloccato
Monitoraggio di eventi critici 🚨
Monitoraggio di eventi critici 🚨
1
Compito
JAVA 25 SELF, livello 52, lezione 3
Bloccato
Gestione dell'accesso alle piste di decollo e atterraggio ✈️
Gestione dell'accesso alle piste di decollo e atterraggio ✈️
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION