Semafori in Java

Definizione Semafori in Java

I semafori sono una tecnologia di sincronizzazione utilizzata per controllare l’accesso a risorse condivise in un programma multithreading. In Java, i semafori possono essere utilizzati tramite la classe java.util.concurrent.Semaphore.

Per utilizzare un semaforo in Java, è necessario creare un’istanza della classe Semaphore, specificando il numero di thread che possono accedere alla risorsa condivisa contemporaneamente. Ad esempio, per creare un semaforo che consente l’accesso a una risorsa da un massimo di 2 thread contemporaneamente, è possibile utilizzare il codice seguente:

import java.util.concurrent.Semaphore;

Semaphore semaphore = new Semaphore(2);

Per acquisire l’accesso alla risorsa condivisa, è necessario chiamare il metodo acquire() del semaforo. Ad esempio:

semaphore.acquire();


Questo metodo bloccherebbe il thread corrente fino a quando non ci sarà un posto libero nel semaforo.

Per rilasciare l’accesso alla risorsa condivisa, è necessario chiamare il metodo release() del semaforo. Ad esempio:

semaphore.release();

E’ importante notare che il metodo release() deve essere chiamato solo se il metodo acquire() è stato chiamato prima.

E’ possibile utilizzare i semafori per gestire l’accesso a risorse condivise in un programma multithreading in modo sicuro ed efficace, prevenendo race condition e deadlock.

Esempio di utilizzo:

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); // 3 è il numero di permessi disponibili
        
        // creiamo 10 thread che cercano di ottenere un permesso
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // cerca di ottenere un permesso
                    System.out.println(Thread.currentThread().getName() + " ha ottenuto un permesso.");
                    Thread.sleep(1000); // simula l'esecuzione di un'operazione
                    semaphore.release(); // rilascia il permesso
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

In questo esempio, abbiamo creato un oggetto Semaphore con 3 permessi disponibili. Poi abbiamo creato 10 thread che cercano di ottenere un permesso chiamando il metodo acquire(). Se un thread non riesce ad ottenere un permesso, esso rimane in attesa fino a quando un permesso non è disponibile. Una volta che un thread ottiene un permesso, esso può eseguire l’operazione per cui ha bisogno del permesso e poi lo rilascia chiamando il metodo release().

Esempio pratico : accesso alla stampante

import java.util.concurrent.Semaphore;

public class Printer {
    private Semaphore semaphore;

    public Printer() {
        semaphore = new Semaphore(1); // 1 permesso disponibile
    }

    public void print(String document) {
        try {
            semaphore.acquire(); // cerca di ottenere un permesso
            System.out.println(Thread.currentThread().getName() + " sta stampando: " + document);
            Thread.sleep(1000); // simula il tempo di stampa
            System.out.println(Thread.currentThread().getName() + " ha finito di stampare: " + document);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // rilascia il permesso
        }
    }

    public static void main(String[] args) {
        Printer printer = new Printer();

        // creiamo 10 thread che cercano di stampare un documento
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                printer.print("documento" + Thread.currentThread().getName());
            }).start();
        }
    }
}

In questo esempio abbiamo creato una classe Printer con un semaforo che controlla l’accesso alla stampante. Il costruttore di Printer crea un semaforo con un solo permesso disponibile, significa che solo un thread può stampare alla volta. Il metodo print() cerca di ottenere un permesso prima di stampare, e rilascia il permesso una volta che ha finito di stampare. In questo modo, solo un thread alla volta può stampare e gli altri devono aspettare.

Esempio pratico: Pool di connessioni

import java.util.concurrent.Semaphore;

public class ConnectionPool {
    private Semaphore semaphore;
    private int connections = 0;
    private int maxConnections;

    public ConnectionPool(int maxConnections) {
        this.maxConnections = maxConnections;
        semaphore = new Semaphore(maxConnections);
    }

    public void acquireConnection() {
        try {
            semaphore.acquire();
            connections++;
            System.out.println(Thread.currentThread().getName() + " ha ottenuto una connessione. Connessioni attive: " + connections);
            Thread.sleep(1000); // simula l'utilizzo di una connessione
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void releaseConnection() {
        connections--;
        System.out.println(Thread.currentThread().getName() + " ha rilasciato una connessione. Connessioni attive: " + connections);
        semaphore.release();
    }

    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool(5);

        // creiamo 10 thread che cercano di ottenere una connessione
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                pool.acquireConnection();
                pool.releaseConnection();
            }).start();
        }
    }
}

In questo esempio abbiamo creato una classe ConnectionPool che gestisce un pool di connessioni. Il costruttore di ConnectionPool crea un semaforo con un numero di permessi pari al numero massimo di connessioni disponibili. Il metodo acquireConnection() cerca di ottenere un permesso prima di creare una nuova connessione, e rilascia il permesso una volta che ha finito di utilizzare la connessione. In questo modo, solo un numero limitato di thread può ottenere una connessione contemporaneamente e gli altri devono aspettare fino a quando una connessione non è disponibile.

Utilizzo dell’interfaccia Runnable

Nell’esempio seguente, abbiamo creato una classe Task che implementa l’interfaccia Runnable e utilizza un semaforo per acquisire e rilasciare l’accesso alla risorsa condivisa.

class Task implements Runnable {
    private Semaphore semaphore;
    private String taskName;

    public Task(Semaphore semaphore, String taskName) {
        this.semaphore = semaphore;
        this.taskName = taskName;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(taskName + " ha acquisito l'accesso alla risorsa");
            Thread.sleep(1000);
            System.out.println(taskName + " ha rilasciato l'accesso alla risorsa");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


In questo esempio, quando un thread chiama il metodo acquire() del semaforo, esso verrà bloccato fino a quando non sarà disponibile un posto libero nel semaforo. Una volta che il thread ha acquisito l’accesso alla risorsa, esso esegue le proprie operazioni, in questo caso stampa un messaggio e dorme per 1 secondo. Infine, il thread chiama il metodo release() per rilasciare l’accesso alla risorsa e permettere ad altri thread di accedervi.

E’ possibile utilizzare anche il metodo tryAcquire() per tentare di acquisire l’accesso alla risorsa senza bloccarsi, ad esempio:

if (semaphore.tryAcquire()) {
    // operazioni sulla risorsa condivisa
    semaphore.release();
} else {
    // gestione del caso in cui l'accesso alla risorsa non è stato acquisito
}


In questo modo si evita il blocco del thread se non c’è un posto libero nel semaforo

Inoltre, è possibile utilizzare il metodo availablePermits() per ottenere il numero di posti liberi nel semaforo e il metodo drainPermits() per acquisire tutti i posti disponibili nel semaforo in una sola volta.

Per quanto riguarda la gestione dei thread, i semafori sono uno strumento molto utile per garantire l’accesso alla risorse condivise, ma è importante utilizzarli con attenzione per evitare problemi di sincronizzazione come race condition e deadlock.

La mia repository GitHub sulla lezione

API classe Semaphore

Pubblicato da Carlo Contardi

Carlo Contardi, docente di informatica e sviluppatore Full Stack, condivide la sua passione per la programmazione e l’informatica attraverso il suo blog Space Coding. Offre preziosi consigli e soluzioni pratiche a chi vuole imparare a programmare o migliorare le proprie abilità. 🚀👨‍💻

Translate »