Progetto Filosofi a cena - Gianmarco-Rampulla-JCMaxwell-4Bi/MultiThreading GitHub Wiki

Progetto Filosofi a cena

I concetti imparati dallo sviluppo del programma sono i seguenti:

  • il problema dei filosofi a cena,
  • uso delle enumerazioni in Java,
  • uso di wait(),
  • uso di notify(),
  • uso di notifyAll().

Stai cercando i concetti imparati dal progetto TicTacToe? clicca qui sotto! :

https://github.com/Gianmarco-Rampulla-JCMaxwell-4Bi/MultiThreading/wiki

Il problema dei filosofi a cena

Il problema dei filosofi a cena è un ottimo esempio per imparare la sincronizzazione tra i threads.

Ideato nel 1965 da Edsger Dijkstra, informatico olandese, il problema consiste in 5 filosofi riuniti a tavola con 5 piatti di spaghetti e 5 forchette, una a testa.

I filosofi, come da definizione, pensano e a volte mangiano ma per prendere il cibo hanno bisogno di due forchette ciascuno, quindi, non possono mai servirsi tutti contemporaneamente perchè le forchette in totale sono solo 5.

Una volta prese le forchette i filosofi mangiano per un po' per poi ritornare a pensare.

Ovviamente è necessario che tutti i commensali gradiscano la cena.

Perchè è importante per lo studio dei threads?

E' importante perchè permette di capire come distribuire equamente delle risorse limitate permettendo una collaborazione tra i competitori delle risorse stesse in modo che nessuno rimanga in una fase di stallo (deadlock) e che tutti riescano a servirsi evitando l'impossibilità di usufruire della risorsa stessa (starvation)

Deadlock

Fase in cui due o più thread si bloccano a vicenda, perchè vogliono delle risorse che appartengono al thread opposto.

Starvation

Fase in cui un thread non riesce ad accedere ad una risorsa in quanto sempre utilizzata

Uso delle enumerazioni in Java

La enumerazione in Java è un particolare tipo di variabile che può avere solo un limitato range di valori stabiliti al momento della sua creazione.

Esempio di un'oggetto enum

       public enum Prova{ //anche in questo caso si possono usare gli identificatori di accesso
           //serie di valori che la variabile può assumere
           FUNZIONA  
           NONVA
           ANCORAINTEST
           PERCHENONVA
       }

       public static main(String[] args)
       {
          Prova VariabileEnum; //dichiaro una variabile di tipo Prova (enumerazione) questa variabile può assumere
          solo valori contenuti nella enumerazione

          VariabileEnum = Prova.FUNZIONA // per assegnare un valore dell'enumerazione ad una variabile dello stesso 
          tipo, basta assegnare a quella variabile un valore della enum.

          //per accedere ad un valore della enum bisogna usare questa sintassi: NomeEnumerazione.Valore
      }

Perchè usarle e come confrontare i valori di un enum

Le enumerazioni sono molto utili quando bisogna creare dei programmi che utilizzano valori costanti.

Pensiamo, ad esempio, ad un programma che svolge la funzione di Calendario: essendo i giorni sempre costanti al posto di dichiararli come semplici variabili stringa possiamo dichiararli come enumerazione in modo da rendere più leggibile il codice ed evitare errori.

Esempio

               enum Calendario{
               LUNEDI,
               MARTEDI;
               //gli altri valori.. 
               }
                 
               public static void main(String[] args)
               {
                   Calendario Giorno;
                   Giorno = Giorno.LUNEDI; //posso mettere solo un valore predefinito...
               }

Permettono, inoltre, di confrontare il loro valore in modo molto semplice.

Un modo molto efficace per confrontare il valore di una variabile enum consiste nell'utilizzo dello switch-case:

Esempio di switch-case per il controllo di una variabile enum

         switch (STAfacendo){ //nelle parentesi tonde si mette la variabile enum da confrontare

            case PENSA: //in base al suo valore (ovvero uno dei valori stabiliti) decide che strada prendere

                try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) {}
                STAfacendo = Tavola.Azione.vuolePRENDEREforchette;
                break;

            case vuolePRENDEREforchette: //in questo caso non bisogna usare la sintassi NomeEnumerazione.Valore 

                if (tavolo.prendiFORCHETTE(id)) {
             
                    System.out.println("F[" + id + "] ha PRESO le forchette");
                    STAfacendo = Tavola.Azione.MANGIA;
                } else {
                
                    System.out.println("F[" + id + "] 
                    STAfacendo = Tavola.Azione.ASPETTAforchette;
                }
                break;

            case ASPETTAforchette: //ma bisogna mettere solo il nome del valore
           
                tavolo.aspettaFORCHETTE();
                STAfacendo = Tavola.Azione.vuolePRENDEREforchette;
                break;

            case MANGIA: //in caso di un enum switch usare NomeEnumerazione.Valore risulta errato in quanto non accettato dal compilatore

                try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) {}
                mangiato++;
                System.out.println("F[" + id + "] ha FINITO di MANGIARE la portata N. " + mangiato);
                tavolo.posaFORCHETTE(id);

                if(mangiato == 3) { 
                   
                    STAfacendo = Tavola.Azione.seNEandatoVIA;
                    System.out.println("F[" + id + "] si ALZA da TAVOLA");
                }
                else { STAfacendo = Tavola.Azione.PENSA; } 
                break;

            default:
                System.out.println("ERROR: non mi dovrei trovare qui MAI!!");
                return;

        }

Si possono comunque usare anche dei metodi di comparazione differenti come equals() e la comparazione logica == anche se risultano meno eleganti.

Esempio di equals() e comparazione logica

               enum Calendario{
               LUNEDI,
               MARTEDI;
               //gli altri valori.. 
               }
                 
               public static void main(String[] args)
               {
                   Calendario Giorno;
                   Giorno = Calendario.LUNEDI; //posso mettere solo un valore predefinito...
                   
                  //esempio confronto con ==
                  if(Giorno == Calendario.LUNEDI)
                  {

                  }

                  //esempio confronto con equals
                  if(Giorno.equals(Calendario.LUNEDI))
                  {
                 
                  }
               }               

Enumerazioni avanzate con costruttori e metodi

Le enumerazioni in Java, al contrario di altri linguaggi, possono contenere anche metodi e costruttori:

Come, ad esempio, nel codice di Filosofi a Cena:

       public enum Azione { // variabile enum Azione
     
                //questi sono i valori che questa variabile può assumere
                PENSA                   (0),  // il valore presente nella parentesi tonda invoca il costruttore che inizializza le variabili in modo da associare, al momento dell'assegnazione di una variabile Azione (enum), anche i valori delle variabili interne
                vuolePRENDEREforchette  (1),  // stessa cosa per quanto riguarda gli altri valori
                ASPETTAforchette        (2),  
                MANGIA                  (3), 
                seNEandatoVIA           (4);  

                //ogni dato prima di essere usato per ogni costante deve essere dichiarato all'interno dell'enum
                private final int idx;      // per ogni COSTANTE c'e' una variabile "idx"
    
            
                private Azione (int idx) { //questo e' il costruttore che ci permette l'associazione con ogni costante
                this.idx = idx; //attraverso questo comando si effettua la vera associazione
                }

  
                public int getIDX() { return this.idx; } //possiamo mettere anche dei metodi che ogni variabile di tipo enum potra' usare. 
}

Uso di wait()

Riprendendo il concetto dei monitor già visto nel progetto TicTacToe, wait() è un comando utile per far rilasciare da un thread il lock (ovvero il "possesso") di un metodo synchronized e tenerlo in sospeso.

Immaginando che i thread accedano ad un metodo synchronized come se fossero in fila, il thread che sta utilizzando il metodo all'esecuzione di wait() libera il posto ad un altro thread mettendosi in attesa fino a quando non ritornerà in fila per il metodo attraverso notify() o notifyAll() che vedremo successivamente.

Per fare un'ulteriore paragone supponiamo che un dottore stia visitando un paziente, però c'è un urgenza e non può più visitarlo, la cosa più logica da fare sarebbe farlo aspettare per poi continuare la visita dopo, allo stesso modo durante il metodo può esserci un istruzione wait() che mette in attesa il thread che ha il lock per farne entrare un altro.

Esempio di utilizzo di wait()

      public synchronized void aspettaFORCHETTE() {
            try {
                wait(); //viene invocato, per il thread che sta eseguendo il codice, il metodo wait()
                //quindi lascia il posto e va in attesa di una notifica da un altro thread
            } catch (InterruptedException ex) {
                Logger.getLogger(Tavola.class.getName()).log(Level.SEVERE, null, ex);
            }
            
      } 

N.B. il metodo wait() va sempre usato in un contesto synchronized in quanto, riguardando i thread, può essere molto rischioso eseguire il comando al di fuori di un contesto di questo tipo.

Senza la keyword synchronized, infatti, il thread non competerebbe per nessuna risorsa e, quindi, non avrebbe senso attendere perchè non sarebbe in coda per nessuna risorsa.

Java segnala questa mancanza con l'eccezione: java.lang.IllegalMonitorStateException

Uso di notify()

il comando notify(), invece, avverte un thread in sospeso che può rientrare di nuovo a competere per una risorsa.

Riprendendo l'esempio di prima, in questo caso,è come se il dottore richiamasse i suoi pazienti per far si che si mettano in coda e continuino la visita, a discrezione del dottore che li ha messi in attesa.

La scelta del thread da risvegliare, in questo caso, è totalmente arbitraria e dipende dalla discrezione dell'implementazione.

Esempio di utilizzo di notify()

  private synchronized void ProvaPerGitHub()
  {
      //codice da eseguire
      notify();   //risveglia un thread in attesa 

  }   

N.B. anche in questo caso notify() si utilizza solo in un contesto synchronized per gli stessi motivi di prima.

Uso di notifyAll()

il comando notifyAll() ha la stessa funzione di notify() ma risveglia tutti i thread in attesa che dovranno mettersi di nuovo in coda e competere per la risorsa richiesta.

Esempio di utilizzo di notifyAll()

         public synchronized void posaFORCHETTE (int Sinistra) {

            int Destra = Sinistra + 1;
    
            if (Destra == posti) {
            Destra = 0;
            }
    
            forchetta[Sinistra] = true; 
            forchetta[Destra] = true; 
 
            notifyAll(); // con notifyAll() il thread, a questo punto, comunica a tutti gli altri thread in attesa che la risorsa "forchette" è di nuovo disponibile pronta per essere utilizzata.
    
         }

N.B. vale anche in questo caso l'utilizzo solo in un contesto synchronized.