Home - Gianmarco-Rampulla-JCMaxwell-4Bi/MultiThreading GitHub Wiki

Progetto TicTacToe

I concetti imparati dallo sviluppo del programma sono i seguenti:

  • cosa è un Thread,
  • creazione e dichiarazione di un Thread,
  • uso di thread.start(),
  • uso di thread.join(),
  • uso di thread.interrupt(),
  • interrompere temporaneamente thread usando TimeUnit.MILLISECONDS.sleep(),
  • generazione numeri random,
  • concetto di variabile statica,
  • utilizzo della notazione @Override,
  • come accedere correttamente alle variabili utilizzando i Threads.

Alla fine della pagina è inoltre presente un esempio di esecuzione del programma su NetBeans oltre che un esempio di come l'accesso corretto alle variabili possa influenzare realmente sul funzionamento del programma eseguito sempre sullo stesso IDE.

Stai cercando i concetti imparati dal progetto Filosofi a cena? clicca qui sotto! :

https://github.com/Gianmarco-Rampulla-JCMaxwell-4Bi/MultiThreading/wiki/Progetto-Filosofi-a-cena

Cosa è un Thread

Un Thread è una suddivisione di un programma in uno o più sottoparti in modo da poter essere seguite in modo concorrente. Un sistema in grado di eseguire Thread viene chiamato sistema multi-threading.

Sono molto utili quando c'è la necessità di dover eseguire più operazioni contemporaneamente come ad esempio quando bisogna gestire l'interfaccia grafica di un programma mentre si esegue una ricerca di un valore dato in input.

Senza l'ausilio dei thread il thread principale, che prima si occupava della finestra del programma, effettuerebbe la ricerca dell'input non rispondendo ad esempio al clic su un bottone dell'interfaccia in quanto già occupato.

Quindi per ovviare al problema lo si scompone dando ad un thread la gestione dell'interfaccia e ad un altro l'incarico di effettuare la ricerca.

Un po' come in un ristorante: con un solo cameriere (che in questo caso rappresenta solo il programma senza thread), molti clienti resterebbero molto tempo ad aspettare, quindi, si dà a più camerieri il compito di servire clienti diversi velocizzando il tempo richiesto per accontentarli tutti (mentre in questo ultimo esempio i camerieri rappresenterebbero un programma con più thread).

Creazione e dichiarazione di un Thread

In Java abbiamo appreso che per dichiarare un thread e quindi crearlo bisogna scrivere il seguente codice:

        Thread NomeThread = new Thread(new ClasseCheImplementaRunnable(EventualiParametri));

Dove "ClasseCheImplementaRunnable" è una classe personalizzata che implementa Runnable.

Come ad esempio:

      class ProvaPerGitHub implements Runnable{
      }

Gli "EventualiParametri" ovviamente dipendono dal costruttore della classe che implementa Runnable.

In caso si costruisse una classe con questo tipo di costruttore:

  class ProvaPerGitHub implements Runnable{
       
       public ProvaPerGitHub(String Messaggio)
       {
       }
  }

In questo caso la dichiarazione del thread dovrà contenere anche i parametri da passare al costruttore della classe che implementa Runnable.

Usando questo esempio la dichiarazione di un thread con questa classe sarebbe:

              Thread NomeThread = new Thread(new ProvaPerGitHub("Ciao"));

Curiosità: si può, al posto di scrivere implements Runnable, ereditare la classe Thread utilizzando la keyword extends.

Esempio di classe ereditata da Thread

class ProvaPerGitHub extends Thread{
       
        public ProvaPerGitHub(String Messaggio)
        {
        }
   }

Uso di thread.start()

Una volta dichiarati i Thread, prima di poter vedere veramente una suddivisione del lavoro, bisogna farli partire.

Per farlo, in Java, si utilizza il comando Thread.start().

Esempio usando il thread di prima:

  NomeThread.start() //faccio partire il rispettivo thread

Questa procedura, però, non fa altro che invocare il metodo run() all'interno della classe passata come parametro alla creazione del Thread, che deve per forza contenere un metodo con lo stesso nome all'interno della classe.

Infatti all'interno della classe del thread (quella che implementa Runnable) bisogna aggiungere:

     class ProvaPerGitHub implements Runnable{
           
          public ProvaPerGitHub(String Messaggio)
          {
          }

          @Override //verra' spiegato dopo
          public run()
          {
           //codice da far eseguire all'avvio del Thread
          }


      }

Per fare in modo che all'invocazione di start() il thread faccia qualcosa basta inserire il codice all'interno di run().

Dal momento dell'avvio il thread sarà completamente indipendente dal thread principale (il programma all'avvio) e smetterà di esistere o quando avrà finito ciò che deve fare o in casi particolari in cui verrà richiesta l'interruzione.

Curiosità: l'esempio è valido anche per le classi che estendono Thread ma è, comunque, consigliabile utilizzare implements Runnable per alcuni vantaggi.

Ad esempio:

  • se si estende Thread non si potrebbero più ereditare altre classi (Java non supporta l'ereditarietà da classi multiple) mentre con implements Runnable, essendo un'interfaccia, si potrebbe ereditare anche altre classi.

Per maggiori informazioni: https://manikandanmv.wordpress.com/tag/extends-thread-vs-implements-runnable/

Uso di thread.join()

Dal momento dall'avvio di un thread, il processo principale ovvero il programma continua la sua esecuzione. Quindi se dopo la chiamata allo start() dei vari thread c'e' del codice verrà eseguito.

Ma se volessimo aspettare che un particolare thread o magari tutti i thread finiscano prima di continuare con l'esecuzione del processo principale o di un thread in generale?

Ecco che ci viene in aiuto thread.join().

Questo metodo permette, infatti, di aspettare che un thread finisca per andare avanti con l'elaborazione del thread da cui è invocato.

Esempio senza join():

     public class ProvaPerGitHubMain
     {
       public static void Main(String[] args)
       {
            Thread Prova1 = new Thread(new ProvaPerGitHub("StringaInutile"));
            Prova1.start(); //esegue il thread 
             
            //non attende che il thread finisce                

            System.out.println("Fine di tutti i thread") //viene eseguito subito

       }  
     }

    class ProvaPerGitHub implements Runnable{
           
          public ProvaPerGitHub(String Messaggio)
          {
              
          }

          @Override //verra' spiegato dopo
          public run()
          {
           System.out.println("Esecuzione Thread!"); //codice di prova
          }


      }

Esempio con join():

     public class ProvaPerGitHubMain
     {
       public static void Main(String[] args)
       {
            Thread Prova1 = new Thread(new ProvaPerGitHub("StringaInutile"));
            Prova1.start(); //esegue il thread 
             
            Prova1.join(); //ferma l'esecuzione del programma e continua quando Prova1 ha finito o viene interrotto           

            System.out.println("Fine di tutti i thread") //viene eseguito dopo il join

       }  
     }

    class ProvaPerGitHub implements Runnable{
           
          public ProvaPerGitHub(String Messaggio)
          {
              
          }

          @Override //verra' spiegato dopo
          public run()
          {
           System.out.println("Esecuzione Thread!"); //codice di prova
          }


      }

Ultima cosa ma non meno importante riguarda il try-catch con cui è necessario circondare thread.join().

Esempio del try-catch da utilizzare con join():

    public class ProvaPerGitHubMain
     {
       public static void Main(String[] args)
       {
            Thread Prova1 = new Thread(new ProvaPerGitHub("StringaInutile"));
            Prova1.start(); //esegue il thread 
             
            try
            {
            Prova1.join(); //ferma l'esecuzione del programma e continua quando Prova1 ha finito o viene interrotto    
            }
            catch (InterruptedException ex) {
          System.out.println("Thread Interrotto!"); //se il thread da cui si e' invocata la procedura join viene interrotto 
        viene segnalato
         }       

           

       }  
     }

    class ProvaPerGitHub implements Runnable{
           
          public ProvaPerGitHub(String Messaggio)
          {
              
          }

          @Override //verrà spiegato dopo
          public run()
          {
           System.out.println("Esecuzione Thread!"); //codice di prova
          }


      }

L'eccezione InterruptedException viene lanciata quando il thread da cui viene invocato join() viene interrotto.

Uso di thread.interrupt()

thread.interrupt(), invece, è totalmente l'opposto di join().

Infatti, se è possibile cerca di fermare prematuramente il thread da cui viene invocato.

In verità non interrompe immediatamente il thread in quanto farlo non è sicuro e porterebbe più svantaggi che vantaggi.

Bensì attiva un flag (una variabile booleana) all'interno del thread in modo da far capire che il thread dovrebbe essere interrotto. Quindi solo in casi possibili all'attivazione del flag il thread viene realmente interrotto. per fare in modo che l'interruzione sia possibile bisogna scrivere del codice in modo da controllare il flag e indirizzare il thread in una corretta interruzione.

Esempi di codice per interrompere un thread:

  • Metodi bloccanti:

     //in questo caso, se invoco Prova1.interrupt()
        try {
         
          Prova1.sleep((long) 10000); // metodo bloccante che lancia un eccezione se viene richiesta l'interruzione
          
         
       } catch (InterruptedException e) { //viene catturata l'eccezione
           System.out.println("Thread Terminato");
           return //il thread ritorna cioè si interrompe
       }        
    
  • Controllo nel run():

       @Override
            public void run() { //nel run() della classe del thread
                  while (!Thread.currentThread().isInterrupted()) { 
                  //si esegue un controllo per cui si esegue il codice solo se il flag di interrupted() non è attivo
                                                                  
                  }//se il codice viene eseguito solo quando non e' interrotto, se lo diventa esce dal ciclo
           }//uscito dal ciclo, se non è presente ulteriore codice uscirà dal run() e si chiuderà il thread
    

Interrompere temporaneamente un thread usando TimeUnit.MILLISECONDS.sleep();

TimeUnit.MILLISECONDS.sleep() permette di fermare temporaneamente il thread da cui è stato invocato.

Come parametro richiede il numero, in questo caso, di millisecondi.

Esempio utilizzo TimeUnit:

       TimeUnit.MILLISECONDS.sleep(300) //ferma il thread corrente per 300 millisecondi

E' simile a thread.sleep() trattato precedentemente e come quest'ultimo deve essere circondato da try-catch

Esempio try-catch con TimeUnit:

        try {
            TimeUnit.MILLISECONDS.sleep(300); //aspetta tempo casuale
        } catch (InterruptedException e) {
            //inserire codice da eseguire in caso di interruzione
           
        }

Quindi anche in questo caso se nel thread che sta "dormendo" viene attivato il flag del thread.interrupt() verrà catturata l'eccezione in modo da poter eseguire del codice per gestire la richiesta di interruzione.

Curiosità: TimeUnit è una classe che contiene altre unità di misura come SECONDS o MINUTES quindi se voglio aspettare un minuto posso scrivere:

      TimeUnit.MINUTES.sleep(nminuti) //nminuti può essere una variabile o, ovviamente, un valore già definito

NB: per usare TimeUnit bisogna importare la seguente libreria: java.util.concurrent.TimeUnit

Generazione numeri random

Uno dei modi possibili per generare numeri casuali è utilizzare Math.random().

  NumeroDaCuiPartire + (Math.random() * NumeroMassimo);

Dove NumeroDaCuiPartire rappresenta il minimo valore da cui partire a sorteggiare (può essere sia una variabile che un valore già definito).

Mentre NumeroMassimo rappresenta il valore massimo che può uscire dal sorteggio (può essere sia una variabile che un valore già definito anche in questo caso).

Esempio con Math.Random():

      int Random = 10 + (Math.random() * 100); //sorteggia un numero casuale da 10 a 100

Può essere anche usato in questi modi:

  • Solo specificando il NumeroMassimo

      int Random = Math.random() * 100; //sorteggia un numero casuale da 0 a 100
    
  • Usando solo Math.random()

      int Random = Math.random(); //sorteggia un numero casuale da 0 a 1
    

Curiosità: è possibile generare numeri casuali anche attraverso la classe Random.

Per usare questa classe bisogna importare java.util.Random.

Passaggi per usare la classe Random:

  • Istanziare oggetto di Classe Random:

            Random C = new Random();
    
  • usare la funzione nextInt() per generare un numero intero casuale:

         int k = random.nextInt((NumeroMassimo))+NumeroDaCuiPartire;
    

Dove:

  • NumeroMassimo è opzionale ed indica il numero massimo entro cui estrarre il numero.
  • NumeroDaCuiPartire è opzionale ed indica il numero minimo entro cui estrarre il numero.

Curiosità: La classe Random contiene anche metodi per sorteggiare casualmente valori Boolean, Float e molti altri.

Concetto di variabile statica

Le variabili statiche, al contrario di quelle normali, vengono istanziate solo una volta quindi ne è disponibile solo una istanza per tutta la classe in cui risiede.

Vengono usate per scambiare informazioni tra i thread in quanto, essendo statica e quindi non avendo copie, tutti i thread possono accendere a questo tipo di variabile.

Per dichiarare una variabile statica in Java:

(ModificatoreDiAccesso) static TipoVariabile NomeVariabile;

Dove

  • ModificatoreDiAccesso sarebbe la keyword che indica la visibilità della variabile(ad esempio: public, private, protected), si può anche omettere.

  • TipoVariabile sarebbe il tipo di dato (esempio: int, String ecc).

  • NomeVariabile rappresenta il nome con cui poi richiamare la variabile.

Per accedere a queste variabili, se sono static, public e contenute in altre classi, si dovrà usare la notazione "NomeClasse"."NomeVariabileStatica" per accedere dall'esterno alla classe contenente la variabile statica

Esempio di accesso ad una variabile esterna statica:

         public class ProvaPerGitHub
         {
            public static int ciao = 0; //dichiarazione variabile intera
         }

         class VoglioUnaVariabileStatic
         {
             //se voglio accedere alla variabile "ciao" contenuta in "ProvaPerGitHub"
             //essendo all'esterno della classe in cui è stata dichiarata
             //devo scrivere:

             ProvaPerGitHub.ciao = 3; //assegnazione per dimostrare l'accesso alla variabile

             //NB: questa notazione funziona solo se la variabile è dichiarata come pubblica
             //altrimenti non funzionerebbe in quanto non sarebbe visibile all'esterno della classe in cui è stata dichiarata
         }              

Se invece si accede dalla stessa classe in cui è stata dichiarata, si utilizza come una variabile normale.

Esempio accesso variabile statica all'interno della stessa classe:

        public class ProvaPerGitHub
         {
            public static int ciao = 0; //dichiarazione variabile intera
            
            public static void Main(String[] args)
            {
            ciao = 3; //non c'è bisogno di scrivere di nuovo classe, essendo il metodo all'interno della classe con la variabile statica
            } 
         }

NB: le variabile statiche non possono essere dichiarate nei metodi, ma solo all'interno di una classe e fuori da qualsiasi metodo

Esempio dichiarazione variabile statica:

      public class ProvaPerGitHub
         {
            public static int ciao = 0; //dichiarazione corretta

             private void NonPuoi()
             {
                 public static int ciao = 0; //dichiarazione incorretta
             }

         }

Utilizzo della notazione @Override

La notazione @Override, serve per sovrascrivere un metodo che è stato ereditato da una classe o implementato da un interfaccia.

In questo modo, il metodo di cui è stato effettuato l'override è indipendente dal metodo della classe padre (da cui eredita).

Se non scrivessimo @Override il compilatore non sovrascriverebbe il metodo ma effettuerebbe un Overload del metodo generando un errore.

Per Overload del metodo si intende un metodo con lo stesso nome ma con una firma differente.

In semplici parole è come avere tanti ristoranti che servono la stessa pietanza ma richiedono metodi di pagamento diversi.

Esempio utilizzo @Override:

      class ProvaPerGitHub implements Runnable //implementa un interfaccia, richiede l'implementazione di alcuni metodi 
      obbligatoriamente
       {

              @Override //notazione override, indica al compilatore che deve sovrascrivere questo metodo implementato 
              dall'interfaccia Runnable, se non ci fosse il compilatore lancerebbe un eccezione in quanto deve essere per forza overridato

                public void run() { //nel run() della classe del thread
                


                }
       }

Per ulteriori informazioni sul motivo per cui è consigliabile usare @Override -> http://lancill.blogspot.it/2012/11/annotations-override.html

Esempio di esecuzione del codice in NetBeans

Foto

Esempio di esecuzione su NetBeans

Trascrizione Foto

In caso non riuscissi a vedere la foto qui trovi la sua trascrizione:

  run:
  Main Thread iniziata...
  <TOE> TOE: 10
  <TAC> TAC: 10
  <TIC> TIC: 10
  <TOE> TOE: 9
  <TAC> TAC: 9
  <TIC> TIC: 9
  <TOE> TOE: 8
  <TAC> TAC: 8
  <TOE> TOE: 7
  <TIC> TIC: 8
  <TAC> TAC: 7
  <TOE> TOE: 6
  <TIC> TIC: 7
  <TOE> TOE: 5
  <TAC> TAC: 6
  <TIC> TIC: 6
  <TOE> TOE: 4
  <TAC> TAC: 5
  <TOE> TOE: 3
  <TIC> TIC: 5
  <TAC> TAC: 4
  <TOE> TOE: 2
  <TIC> TIC: 4
  <TAC> TAC: 3
  <TOE> TOE: 1
  <TIC> TIC: 3
  <TAC> TAC: 2
  <TAC> TAC: 1
  <TIC> TIC: 2
  <TIC> TIC: 1
  Thread Terminati!
  Punteggio: 5
  Main Thread completata! tempo di esecuzione: 2934ms
  BUILD SUCCESSFUL (total time: 3 seconds)

Come accedere correttamente alle variabili utilizzando i Threads

Anche se le variabili statiche sono un metodo efficace per scambiare dati tra i threads, molte volte capita che i due thread eseguino lo stesso pezzo di codice contemporaneamente.

Eseguendo lo stesso pezzo di codice contemporaneamente entrambi possono modificare le stesse variabili, rischiando di sovrascrivere dati già presenti che potrebbero essere molto importanti per la correttezza del programma.

Possiamo paragonare questa situazioni a due professori che correggono un unico compito, entrambi sovrascrivono le correzioni dell'altro non ottenendo un risultato unico e giusto.

Come risolvere?

Per risolvere bisogna fare in modo che i threads eseguino uno alla volta il pezzo di codice da eseguire.

Per farlo bisogna inserire la parola chiave synchronized quando si dichiara una procedura all'interno della classe che viene definita, al momento della dichiarazione di un metodo synchronized, come classe Monitor.

Esempio dichiarazione di una procedura (o funzione) synchronized

      public synchronized void Spiegazione()
      {
         //codice 
      }

Con la parola chiave synchronized solo un thread alla volta può eseguire il codice all'interno della procedura e modificare le variabili interne alla classe, evitando cosi che si verifichi l'esempio di prima.

Il funzionamento dei metodi synchronized è simile a quello di una coda, ovvero il primo thread che richiede l'accesso esegue il codice, gli altri thread aspettano il loro turno fino a quando non viene liberato l'accesso ed eseguire a loro volta il codice.

Come ho già detto la classe che contiene dei metodi synchronized è detta classe Monitor

Esempio di classe Monitor

  class IoControlloIThread //normale dichiarazione di una classe
  {
      private synchronized void Ciao() //essendoci un metodo synchronized questa classe è un Monitor
      {

      }

  }

Esempio del funzionamento del programma con e senza synchronized

Esempio del metodo con keyword synchronized

Qui il codice viene eseguito con il metodo avente la keyword synchronized e non ci sono conflitti tra i thread, infatti, il risultato è pertinente a quanto aspettato

CodiceGiusto

Esempio del metodo senza keyword synchronized

Mentre qui è il codice con il metodo senza la keyword synchronized. Come possiamo vedere il punteggio viene aumentato anche se TOC non viene subito dopo TAC. Segno di un conflitto tra i thread

CodiceSbagliato

Trascrizione dell'Esempio del funzionamento del programma con e senza synchronized

Nel caso non si vedessero le foto ecco qui una trascrizione delle suddette:

Esempio del metodo con keyword synchronized

 Main Thread iniziata...
 <TIC> TIC:10
 <TAC> TAC:10
 <TOE> TOE:10 Punteggio +1
 <TAC> TAC:9
 <TIC> TIC:9
 <TAC> TAC:8
 <TOE> TOE:9 Punteggio +1
 <TAC> TAC:7
 <TIC> TIC:8
 <TAC> TAC:6
 <TOE> TOE:8 Punteggio +1
 <TAC> TAC:5
 <TIC> TIC:7
 <TAC> TAC:4
 <TOE> TOE:7 Punteggio +1
 <TAC> TAC:3
 <TIC> TIC:6
 <TAC> TAC:2
 <TOE> TOE:6 Punteggio +1
 <TAC> TAC:1
 <TIC> TIC:5
 <TOE> TOE:5
 <TIC> TIC:4
 <TOE> TOE:4
 <TIC> TIC:3
 <TOE> TOE:3
 <TIC> TIC:2
 <TOE> TOE:2
 <TIC> TIC:1
 <TOE> TOE:1
 Thread Terminati!
 Punteggio: 5
 Main Thread completata! tempo di esecuzione: 7030ms
 BUILD SUCCESSFUL (total time: 7 seconds)

Esempio del metodo senza keyword synchronized

   Main Thread iniziata...
   <TAC> TAC:10
   <TOE> TOE:10
   <TIC> TIC:10
   <TAC> TAC:9
   <TIC> TIC:9
   <TAC> TAC:8
   <TOE> TOE:9 Punteggio +1
   <TAC> TAC:7
   <TOE> TOE:8 Punteggio +1
   <TIC> TIC:8
   <TAC> TAC:6
   <TIC> TIC:7
   <TOE> TOE:7 Punteggio +1
   <TIC> TIC:6
   <TAC> TAC:5
   <TIC> TIC:5
   <TOE> TOE:6
   <TAC> TAC:4
   <TIC> TIC:4
   <TOE> TOE:5
   <TAC> TAC:3
   <TIC> TIC:3
   <TAC> TAC:2
   <TOE> TOE:4
   <TIC> TIC:2
   <TAC> TAC:1
   <TOE> TOE:3 Punteggio +1
   <TIC> TIC:1
   <TOE> TOE:2
   <TOE> TOE:1
   Thread Terminati!
   Punteggio: 4
   Main Thread completata! tempo di esecuzione: 2809ms
   BUILD SUCCESSFUL (total time: 2 seconds)

Bibliografia

⚠️ **GitHub.com Fallback** ⚠️