Skip to content

Instantly share code, notes, and snippets.

@salvatorecapolupo
Created February 17, 2026 07:03
Show Gist options
  • Select an option

  • Save salvatorecapolupo/f20f2f79064a49fade36a9ebb4995fbb to your computer and use it in GitHub Desktop.

Select an option

Save salvatorecapolupo/f20f2f79064a49fade36a9ebb4995fbb to your computer and use it in GitHub Desktop.
Problema dei 5 filosofi a cena

Parte 1: Base - Creare e avviare un Thread

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!");
    }
}

Parte 2: Intermedia - Risorse Condivise e synchronized

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

Parte 3: Soluzione Completa "5 Filosofi"

Questa soluzione affronta i requisiti richiesti:

  1. Mutua Esclusione: Usiamo ReentrantLock.
  2. No Deadlock: Usiamo la Gerarchia delle Risorse (si prende sempre prima la forchetta con ID più basso). Questo rompe la ciclicità dell'attesa.
  3. No Starvation (Fairness): Usiamo new ReentrantLock(true). Il parametro true attiva 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.

Il Codice Completo

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.");
        }
    }
}

Proposte di lavoro

  • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment