In Java, il modo più semplice per creare un thread è estendere la classe Thread e sovrascrivere il metodo run().
Concetto chiave: start() avvia un nuovo flusso di esecuzione parallelo, mentre chiamare direttamente run() eseguirebbe il codice nel thread principale (sequenziale).
// 1. Definiamo un task semplice
class SalutoTask extends Thread {
private String nome;
public SalutoTask(String nome) {
this.nome = nome;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Ciao dal thread " + nome + " - contatore: " + i);
try {
// Simuliamo un po' di tempo di elaborazione
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread " + nome + " TERMINATO.");
}
}
public class EsempioBase {
public static void main(String[] args) {
SalutoTask t1 = new SalutoTask("A");
SalutoTask t2 = new SalutoTask("B");
// Avviamo i thread in parallelo
t1.start();
t2.start();
System.out.println("Main: Thread avviati!");
}
}Il problema principale dei thread è quando provano a modificare la stessa variabile contemporaneamente (Race Condition).
Concetto chiave: La keyword synchronized garantisce che solo un thread alla volta possa eseguire quel blocco di codice.
class ContatoreCondiviso {
private int count = 0;
// Metodo sincronizzato: solo un thread entra qui alla volta
public synchronized void incrementa() {
count++;
}
public int getCount() {
return count;
}
}
class Incrementatore extends Thread {
ContatoreCondiviso contatore;
public Incrementatore(ContatoreCondiviso c) {
this.contatore = c;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
contatore.incrementa();
}
}
}
public class EsempioIntermedio {
public static void main(String[] args) throws InterruptedException {
ContatoreCondiviso c = new ContatoreCondiviso();
Thread t1 = new Incrementatore(c);
Thread t2 = new Incrementatore(c);
t1.start();
t2.start();
// join() aspetta che i thread finiscano prima di stampare il risultato
t1.join();
t2.join();
System.out.println("Valore finale (dovrebbe essere 2000): " + c.getCount());
}
}Questa soluzione affronta i requisiti richiesti:
- Mutua Esclusione: Usiamo
ReentrantLock. - No Deadlock: Usiamo la Gerarchia delle Risorse (si prende sempre prima la forchetta con ID più basso). Questo rompe la ciclicità dell'attesa.
- No Starvation (Fairness): Usiamo
new ReentrantLock(true). Il parametrotrueattiva la policy "fair": il lock viene dato al thread che aspetta da più tempo (FIFO), impedendo che un filosofo veloce "rubi" sempre la forchetta a uno lento.
Copia questo codice in un file chiamato CenaDeiFilosofi.java.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
// Classe che rappresenta la risorsa (Forchetta)
class Forchetta {
private final int id;
// Lock con "fairness" a true per evitare starvation
private final Lock lock = new ReentrantLock(true);
public Forchetta(int id) {
this.id = id;
}
public boolean prendi() {
return lock.tryLock();
}
public void prendiBloccante() {
lock.lock();
}
public void lascia() {
lock.unlock();
}
public int getId() {
return id;
}
}
class Filosofo extends Thread {
private final int id;
private final Forchetta forchettaSinistra;
private final Forchetta forchettaDestra;
private int pastiConsumati = 0;
private final Random random = new Random();
// Variabile per fermare il thread pulitamente
private volatile boolean running = true;
public Filosofo(int id, Forchetta sinistra, Forchetta destra) {
this.id = id;
this.forchettaSinistra = sinistra;
this.forchettaDestra = destra;
}
private void azione(String azione) throws InterruptedException {
// Simuliamo il tempo dell'azione
int tempo = 200 + random.nextInt(300); // Tra 200ms e 500ms
Thread.sleep(tempo);
}
@Override
public void run() {
try {
while (running) {
// 1. PENSARE
System.out.println("Filosofo " + id + " sta pensando...");
azione("pensando");
// 2. PRENDERE LE FORCHETTE (Strategia Gerarchica anti-deadlock)
// Si prende sempre prima la forchetta con ID più basso
Forchetta prima, seconda;
if (forchettaSinistra.getId() < forchettaDestra.getId()) {
prima = forchettaSinistra;
seconda = forchettaDestra;
} else {
prima = forchettaDestra;
seconda = forchettaSinistra;
}
System.out.println("Filosofo " + id + " vuole forchetta " + prima.getId());
prima.prendiBloccante();
System.out.println("Filosofo " + id + " HA PRESO forchetta " + prima.getId());
System.out.println("Filosofo " + id + " vuole forchetta " + seconda.getId());
seconda.prendiBloccante();
System.out.println("Filosofo " + id + " HA PRESO forchetta " + seconda.getId());
// 3. MANGIARE (Sezione Critica)
pastiConsumati++;
System.out.println(">>> FILOSOFO " + id + " STA MANGIANDO (Totale pasti: " + pastiConsumati + ") <<<");
azione("mangiando");
// 4. POSARE LE FORCHETTE
seconda.lascia();
System.out.println("Filosofo " + id + " ha posato forchetta " + seconda.getId());
prima.lascia();
System.out.println("Filosofo " + id + " ha posato forchetta " + prima.getId());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Filosofo " + id + " interrotto.");
}
}
public void ferma() {
running = false;
}
}
public class CenaDeiFilosofi {
public static void main(String[] args) throws InterruptedException {
int numeroFilosofi = 5;
Filosofo[] filosofi = new Filosofo[numeroFilosofi];
Forchetta[] forchette = new Forchetta[numeroFilosofi];
// Creazione delle forchette
for (int i = 0; i < numeroFilosofi; i++) {
forchette[i] = new Forchetta(i);
}
// Creazione dei filosofi e assegnazione forchette
for (int i = 0; i < numeroFilosofi; i++) {
Forchetta sinistra = forchette[i];
Forchetta destra = forchette[(i + 1) % numeroFilosofi];
// Nota: L'ordine in cui passiamo sx e dx qui è logico (vicini di sedia),
// ma all'interno del run() il filosofo le ordinerà per ID per evitare deadlock.
filosofi[i] = new Filosofo(i, sinistra, destra);
filosofi[i].start();
}
System.out.println("--- CENA INIZIATA (Premi Ctrl+C per terminare) ---");
// Lasciamo girare la simulazione per 10 secondi
Thread.sleep(10000);
System.out.println("\n--- FINE TEMPO: STOP AI FILOSOFI ---");
// Fermiamo i thread
for (Filosofo f : filosofi) {
f.ferma();
}
// Attendiamo che finiscano l'ultimo boccone
for (Filosofo f : filosofi) {
f.join();
}
System.out.println("\n--- STATISTICHE FINALI ---");
for (Filosofo f : filosofi) {
// Nota: qui servirebbe un getter per i pasti, ma stampiamo l'ultimo log a video
System.out.println("Filosofo " + f.getName() + " ha terminato.");
}
}
}- Prova a ridurre al minimo e incrementare al massimo il tempo di sleep(), e verifica cosa succede. L'algoritmo deve funzionare sotto qualsiasi ipotesi temporale.
- Prova a randomizzare i tempi di esecuzione
- Ragiona sul criterio di assegnazione delle forchette, e prova a definire la regola sull'indice i
- Immagina di estendere l'algoritmo a N filosofi, con N>5: continua a funzionare?
- Rimodula l'ouput, in modo che sia più facile da leggere (ad esempio creando una tabella o usando Swing)
- Come faccio ad essere sicuro al 100% che il codice sia corretto? (Spoiler: non puoi :-) )
- What-If: per capire a che cosa serve un costrutto come synchronized, prova a toglierlo e a lanciare il programma.
ReentrantLock(true) (Nota: procedura anti-starvation):
Il costruttore new ReentrantLock(true) crea un lock Fair. Se il Filosofo 1 e il Filosofo 3 stanno aspettando la forchetta 2, Java garantisce che la otterrà chi è arrivato prima nella coda di attesa. Senza true, un thread sfortunato potrebbe aspettare all'infinito.
3. Monitoraggio Real-Time:
La riga System.out.println(">>> FILOSOFO " + id + " STA MANGIANDO (Totale pasti: " + pastiConsumati + ") <<<"); ti permette di vedere a console che tutti stanno avanzando. Noterai che i numeri dei pasti crescono in modo equilibrato (es. tutti arrivano a 5, poi a 6, ecc.) grazie alla fairness.