Questo esercizio, intitolato "Competizione mutex", mira a dimostrare l'implementazione della mutua esclusione in Java attraverso la simulazione dell'accesso di più automobili a un ponte a corsia singola.
L'obiettivo principale è gestire l'accesso a una risorsa condivisa (il ponte) in modo che un solo thread (un'automobile) possa utilizzarla contemporaneamente, evitando conflitti.
L'esercizio si articola su tre componenti fondamentali:
Main.java: Funge da punto di ingresso del programma.
Crea un'istanza della classe Ponte inizializzata come MUTEX (valore 1) e avvia 8 thread, ognuno dei quali rappresenta un'automobile che tenta di attraversare il ponte.
Ponte.java: Rappresenta la risorsa critica.
È implementato seguendo la logica di un Semaforo di Dijkstra inizializzato a 1.
Utilizza i metodi synchronized richiedi() (che esegue una wait() se il ponte è occupato) e rilascia() (che esegue una notify() per sbloccare le altre auto in attesa) per regolare il traffico.
Automobile.java: Definisce il comportamento dei singoli thread.
Ogni automobile attende un tempo casuale prima di tentare l'accesso, entra nella sezione critica richiamando ponte.richiedi(), simula il transito con uno sleep e infine libera la risorsa con ponte.rilascia().
In sintesi, l'esercizio illustra come i concetti di sezione critica e semaforo possano essere tradotti in codice Java per coordinare l'esecuzione di processi paralleli.
La classe Main.java rappresenta il punto di ingresso del programma e ha il compito di configurare l'ambiente di simulazione e avviare i thread.
Ecco una spiegazione dettagliata delle sue componenti principali basata sul codice sorgente:
Definizione delle costanti: Viene definita la costante NUMERO_AUTOMOBILI = 8, che stabilisce quante auto (ovvero quanti thread) parteciperanno alla competizione per l'accesso al ponte.
Inizializzazione della risorsa condivisa: All'interno del metodo main, viene creata un'istanza della classe Ponte.
Questa viene inizializzata con il valore Ponte.MUTEX (che equivale a 1), configurando il ponte come una risorsa che può ospitare una sola automobile alla volta.
Ciclo di creazione dei thread: Attraverso un ciclo for che va da 1 a 8, il programma:
Crea un nuovo oggetto Automobile, assegnandogli un nome univoco (es. "auto1", "auto2", ecc.) e il riferimento all'unico oggetto ponte condiviso.
Incapsula l'oggetto Automobile (che implementa Runnable) in un nuovo oggetto Thread.
Invoca il metodo .start(), che avvia effettivamente l'esecuzione parallela di ogni automobile.
In sintesi, il Main prepara la "scena" creando l'oggetto conteso (il ponte) e lanciando simultaneamente le 8 automobili che cercheranno di attraversarlo seguendo le regole di mutua esclusione.
La classe Ponte.java costituisce il cuore logico dell'esercizio, poiché implementa il meccanismo di controllo degli accessi basato su un Semaforo Mutex.
In informatica, un semaforo di questo tipo è un caso particolare del semaforo di Dijkstra inizializzato a 1, utilizzato per garantire che una risorsa sia usata da un solo thread alla volta.
Ecco un'analisi dettagliata del codice:
MUTEX = 1: È una costante statica che definisce il valore iniziale del semaforo per la mutua esclusione.
autoChePossonoAccedere: Questa variabile intera privata funge da contatore per determinare se la risorsa (il ponte) è libera o occupata.
Il costruttore riceve come parametro il numero massimo di auto ammesse. Nel Main.java, viene passato il valore MUTEX, impostando quindi il limite a un'automobile alla volta.
Questo metodo gestisce l'ingresso nella sezione critica:
È dichiarato come synchronized, il che garantisce che il thread che lo esegue possieda il "monitor" (*) dell'oggetto, impedendo interferenze da altri thread durante la verifica dello stato.
Utilizza un ciclo while (autoChePossonoAccedere == 0): se il ponte è già occupato da un'altra auto, il thread corrente invoca wait(). Questo mette il thread in stato di attesa e rilascia temporaneamente il blocco, permettendo ad altri metodi sincronizzati di essere eseguiti.
Quando il thread viene risvegliato e il ponte è libero, decrementa il contatore (autoChePossonoAccedere--), "occupando" effettivamente il ponte.
Questo metodo gestisce l'uscita dalla sezione critica:
Anch'esso è synchronized per garantire l'atomicità dell'operazione.
Incrementa il contatore (autoChePossonoAccedere++), segnalando che il ponte è ora libero.
Invoca notify(), che ha il compito di risvegliare uno dei thread (automobili) precedentemente messi in attesa nel metodo richiedi().
In sintesi, Ponte.java funge da vigile urbano elettronico che mette in coda le automobili quando il ponte è occupato e ne autorizza il transito una alla volta, garantendo la sicurezza e l'assenza di collisioni (conflitti tra thread).
(*) In informatica, un monitor è un costrutto di sincronizzazione ad alto livello che permette a più thread di avere sia la mutua esclusione (accesso esclusivo a una risorsa) sia la capacità di attendere che una determinata condizione si verifichi.
Sebbene nel codice della classe Ponte.java si faccia riferimento a un "Semaforo Mutex", l'implementazione concreta utilizza proprio il meccanismo del monitor nativo di Java.
Ecco come funziona un monitor e come viene applicato in questo esercizio:
Ogni oggetto in Java ha un monitor associato. Quando un metodo è dichiarato come synchronized, un thread deve acquisire il "monitor" di quell'oggetto prima di poter eseguire il codice.
In Ponte.java, i metodi richiedi() e rilascia() sono synchronized.
Questo garantisce che se un'automobile sta controllando se può accedere, nessun'altra può interferire in quel preciso istante.
A differenza di un semplice lock, un monitor permette ai thread di "mettersi in pausa" se la risorsa non è disponibile e di essere "svegliati" quando la situazione cambia. Questo avviene tramite due funzioni chiave presenti nel codice:
wait(): Quando un thread chiama wait(), rilascia temporaneamente il monitor e si mette in coda in uno stato di attesa. Nel tuo codice, l'automobile chiama wait() se il contatore autoChePossonoAccedere è uguale a zero.
notify(): Viene usato per segnalare ai thread in attesa che qualcosa è cambiato. In Ponte.java, quando un'auto finisce il transito e chiama rilascia(), esegue una notify() per risvegliare una delle auto ferme al semaforo.
Il monitor agisce come il gestore interno dell'oggetto Ponte. Esso:
Protegge la variabile autoChePossonoAccedere da accessi simultanei (grazie a synchronized).
Gestisce la coda di thread (le automobili) che non possono ancora passare (grazie a wait()).
Coordina la ripartenza del traffico non appena la risorsa torna libera (grazie a notify()).
Senza il meccanismo del monitor, le automobili dovrebbero continuare a controllare ciclicamente se il ponte è libero (consumando inutilmente CPU), mentre con il monitor esse "dormono" finché non vengono esplicitamente richiamate al lavoro.
La classe Automobile.java definisce il comportamento dei singoli thread che competono per l'accesso alla risorsa condivisa (il ponte). Implementa l'interfaccia Runnable, il che permette alle sue istanze di essere eseguite come thread paralleli.
Ecco un'analisi dettagliata del suo funzionamento basata sul codice sorgente:
TEMPO_MAX_ATTESA = 3: Definisce il tempo massimo (in secondi) che un'auto attende casualmente prima di presentarsi al ponte.
TEMPO_MAX_TRANSITO = 5: Definisce la durata massima (in secondi) del transito sul ponte.
nomeAutomobile: Una stringa usata per identificare univocamente ogni veicolo.
ponte: Un riferimento all'oggetto condiviso di tipo Ponte, necessario per richiederne l'accesso e segnalarne il rilascio.
Questo metodo contiene il ciclo di vita del thread e la gestione della sezione critica:
Attesa Iniziale: Prima di tentare l'accesso, ogni automobile attende per un tempo casuale (fino a 3 secondi). Questo serve a simulare l'arrivo asincrono delle auto al ponte.
Ingresso nella Sezione Critica:
L'automobile stampa un messaggio indicando la volontà di accedere.
Chiama il metodo ponte.richiedi(). Se il ponte è occupato, il thread si blocca in questo punto (come spiegato in precedenza per Ponte.java).
Simulazione del Transito: Una volta ottenuto l'accesso, l'automobile stampa un messaggio di transito e "occupa" il ponte per un tempo casuale (fino a 5 secondi) tramite TimeUnit.SECONDS.sleep().
Uscita dalla Sezione Critica:
L'automobile stampa un messaggio di uscita.
Chiama il metodo ponte.rilascia(), che sbloccherà l'eventuale auto successiva in attesa.
La classe sovrascrive il metodo toString() per restituire una stringa facilmente leggibile (es: "Automobile auto1"), facilitando il tracciamento delle attività tramite i messaggi in console.
In sintesi, Automobile.java rappresenta l'entità attiva del programma che, seguendo un protocollo preciso (richiesta -> utilizzo -> rilascio), garantisce che il ponte non venga mai attraversato da più veicoli contemporaneamente.
È importante notare che questo progetto potrebbe essere realizzato in modo molto più semplice e immediato utilizzando la classe predefinita Semaphore (disponibile nel pacchetto java.util.concurrent), che automatizza la gestione dei permessi di accesso. Tuttavia, lo scopo di questo esercizio è costruire manualmente un semaforo mutex all'interno della classe Ponte.java.
Questa implementazione "a mano" è fondamentale per comprendere a fondo il concetto di monitor in Java e come funzionano i suoi meccanismi di base:
synchronized: viene applicato ai metodi richiedi() e rilascia() per garantire l'accesso esclusivo allo stato interno del ponte (la variabile autoChePossonoAccedere), impedendo che più thread interferiscano tra loro contemporaneamente.
wait(): viene utilizzato nel metodo richiedi() per mettere in pausa un thread quando la condizione necessaria (ponte libero) non è soddisfatta, facendolo attendere in una coda gestita dal monitor.
notify() (che rappresenta l'operazione di signal): viene invocato nel metodo rilascia() per segnalare ai thread in attesa che la risorsa è stata liberata e che uno di loro può procedere.
In sintesi, sebbene successivamente utilizzeremo strumenti più avanzati, questa modalità permette di "vedere sotto il cofano" e capire come si coordina l'esecuzione parallela attraverso i blocchi fondamentali della sincronizzazione.