Skip to content

7. Thread

7.1. Introduzione

Quando un'applicazione viene avviata, viene eseguita in un flusso di esecuzione chiamato thread. La classe che rappresenta un thread è la classe java.lang.Thread, che presenta le seguenti proprietà e metodi:

currentThread()
restituisce il thread attualmente in esecuzione
setName()
imposta il nome del thread
getName()
nome del thread
isAlive()
indica se il thread è attivo (true) o meno (false)
start()
avvia l'esecuzione di un thread
run()
metodo eseguito automaticamente dopo l'esecuzione del precedente metodo start
sleep(n)
mette in pausa l'esecuzione di un thread per n millisecondi
join()
operazione di blocco: attende che il thread finisca prima di procedere all'istruzione successiva

I costruttori più comunemente utilizzati sono i seguenti:

Thread()
crea un riferimento a un'attività asincrona. Questa attività è ancora inattiva. L'attività creata deve avere un metodo run: molto spesso verrà utilizzata una classe derivata da Thread.
Thread(oggetto Runnable)
Come sopra, ma l'oggetto Runnable passato come parametro implementa il metodo run.

Diamo un'occhiata a una semplice applicazione che dimostra l'esistenza di un thread di esecuzione principale, ovvero quello in cui viene eseguita la funzione main di una classe:

// use of threads

import java.io.*;
import java.util.*;

public class thread1{
    public static void main(String[] arg)throws Exception {
         // init current thread
        Thread main=Thread.currentThread();
         // display
        System.out.println("Thread courant : " + main.getName());
         // we change the name
        main.setName("myMainThread");
         // check
        System.out.println("Thread courant : " + main.getName());

         // infinite loop
        while(true){
        // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // display
            System.out.println(main.getName() + " : " +H);
             // temporary shutdown
            Thread.sleep(1000);
        }//while
    }//hand
}//class

Output sullo schermo:


Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o

L'esempio precedente illustra i seguenti punti:

  • la funzione main viene eseguita correttamente in un thread
  • possiamo accedere alle proprietà di questo thread tramite Thread.currentThread()
  • il ruolo del metodo sleep. In questo caso, il thread che esegue main rimane inattivo per 1 secondo tra una visualizzazione e l'altra.

7.2. Creazione di thread di esecuzione

È possibile avere applicazioni in cui parti di codice vengono eseguite "simultaneamente" in thread di esecuzione diversi. Quando diciamo che i thread vengono eseguiti simultaneamente, spesso usiamo il termine in modo approssimativo. Se la macchina ha un solo processore, come spesso accade, i thread condividono questo processore: ciascuno di essi vi ha accesso, a turno, per un breve istante (pochi millisecondi). Questo è ciò che crea l'illusione dell'esecuzione parallela. La quantità di tempo assegnata a un thread dipende da vari fattori, tra cui la sua priorità, che ha un valore predefinito ma può anche essere impostata a livello di programmazione. Quando un thread ha il processore, normalmente lo utilizza per tutto il tempo a lui assegnato. Tuttavia, può rilasciarlo in anticipo:

  • attendendo un evento (wait, join)
  • mettendosi in standby per un periodo specificato (sleep)
  • Un thread T può essere creato in vari modi
    • estendendo la classe Thread e sovrascrivendo il suo metodo run
    • implementando l'interfaccia Runnable in una classe e utilizzando il costruttore new Thread(Runnable). Runnable è un'interfaccia che definisce un solo metodo: public void run(). L'argomento del costruttore precedente è quindi qualsiasi istanza di classe che implementi questo metodo run.

Nell'esempio seguente, i thread vengono creati utilizzando una classe anonima che estende la classe Thread:

            // create thread i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//def tasks[i]

Il metodo run in questo caso si limita a richiamare il metodo *display*.

  • L'esecuzione del thread T viene avviata da T.start(): questo metodo appartiene alla classe Thread ed esegue una serie di inizializzazioni prima di invocare automaticamente il metodo run del thread o dell'interfaccia Runnable. Il programma che esegue l'istruzione T.start() non attende il completamento del task T: procede immediatamente all'istruzione successiva. Abbiamo quindi due attività in esecuzione in parallelo. Spesso devono essere in grado di comunicare tra loro per conoscere lo stato del lavoro condiviso da svolgere. Questo è il problema della sincronizzazione dei thread.
  • Una volta avviato, il thread viene eseguito in modo autonomo. Si fermerà quando la funzione run che sta eseguendo avrà terminato il proprio lavoro.
  • Possiamo attendere che il thread T finisca di eseguire utilizzando T.join(). Si tratta di un'istruzione di blocco: il programma che la esegue rimane bloccato finché il task T non ha terminato il proprio lavoro. È anche un mezzo di sincronizzazione.

Esaminiamo il seguente programma:

// use of threads

import java.io.*;
import java.util.*;

public class thread2{
    public static void main(String[] arg) {
         // init current thread
        Thread main=Thread.currentThread();
         // give a name to the current thread
        main.setName("myMainThread");
         // start of hand
        System.out.println("début du thread " +main.getName());

         // creation of execution threads
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // create thread i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//def tasks[i]
             // set the thread name
            tâches[i].setName(""+i);
             // start execution of thread i
            tâches[i].start();
        }//for

         // end of hand
        System.out.println("fin du thread " +main.getName());
    }//Main

    public static void affiche() {
         // time recovery
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // display start of execution
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // sleep for 1 s
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // time recovery
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // display end of run
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }// poster
}//class

Il thread principale, che esegue la funzione main, crea altri 5 thread responsabili dell'esecuzione del metodo statico display. I risultati sono i seguenti:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4

Questi risultati sono molto significativi:

  • In primo luogo, vediamo che l'avvio dell'esecuzione di un thread non è bloccante. Il metodo main ha avviato l'esecuzione di 5 thread in parallelo e ha terminato l'esecuzione prima di loro. L'operazione
            // on lance l'exécution du thread i
            tâches[i].start();

avvia l'esecuzione del thread tasks[i], ma una volta fatto ciò, l'esecuzione prosegue immediatamente con l'istruzione successiva senza attendere che il thread finisca.

  • Tutti i thread creati devono eseguire il metodo display. L'ordine di esecuzione è imprevedibile. Anche se nell'esempio l'ordine di esecuzione sembra seguire l'ordine in cui i thread sono stati avviati, non è possibile trarne conclusioni generali. Il sistema operativo in questo caso dispone di 6 thread e un processore. Assegnerà il processore a questi 6 thread secondo le proprie regole.
  • I risultati mostrano l'effetto del metodo sleep. Nell'esempio, il thread 0 è il primo ad eseguire il metodo display. Viene visualizzato il messaggio di inizio esecuzione, poi esegue il metodo sleep, che lo sospende per 1 secondo. A quel punto perde il processore, che diventa disponibile per un altro thread. L'esempio mostra che il thread 1 lo otterrà. Il thread 1 seguirà lo stesso percorso degli altri thread. Quando il periodo di sospensione di 1 secondo per il thread 0 termina, la sua esecuzione può riprendere. Il sistema gli assegna il processore e può completare l'esecuzione del metodo display.

Modifichiamo il nostro programma per terminare il metodo *main* con le seguenti istruzioni:

         // end of hand
        System.out.println("fin du thread " +main.getName());
     // application shutdown
    System.exit(0);

L'esecuzione del nuovo programma produce:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45

Non appena il metodo main esegue l'istruzione:

    System.exit(0);

viene interrotto l'esecuzione di tutti i thread dell'applicazione, non solo del thread principale. Il metodo main potrebbe voler attendere che i thread da esso creati abbiano terminato l'esecuzione prima di terminare a sua volta. Ciò può essere fatto utilizzando il metodo join della classe Thread:

     // waiting for all threads
        for(int i=0;i<tâches.length;i++){
            // we wait for thread i
            tâches[i].join();
    }//for  

         // end of hand
        System.out.println("fin du thread " +main.getName());
     // application shutdown
    System.exit(0);

Questo produce i seguenti risultati:

début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread

7.3. I vantaggi dei thread

Ora che abbiamo evidenziato l'esistenza di un thread predefinito — quello che esegue il metodo Main — e sappiamo come crearne altri, consideriamo i vantaggi dei thread per noi e il motivo per cui li stiamo presentando qui. Esiste un tipo di applicazione che si presta bene all'uso dei thread: le applicazioni client-server su Internet. In un'applicazione di questo tipo, un server situato sulla macchina S1 risponde alle richieste provenienti da client situati su macchine remote C1, C2, ..., Cn.

Image

Utilizziamo ogni giorno applicazioni Internet che seguono questo modello: servizi web, e-mail, navigazione nei forum, trasferimento di file... Nel diagramma sopra, il server S1 deve servire contemporaneamente i client C1, C2, ..., Cn. Se prendiamo l'esempio di un server FTP (File Transfer Protocol) che distribuisce file ai propri client, sappiamo che un trasferimento di file a volte può richiedere diverse ore. È ovviamente fuori discussione che un singolo cliente possa monopolizzare il server per un periodo così lungo. Ciò che si fa di solito è che il server crei tanti thread di esecuzione quanti sono i clienti. Ogni thread è quindi responsabile della gestione di un cliente specifico. Poiché il processore è condiviso ciclicamente tra tutti i thread attivi sulla macchina, il server dedica un po' di tempo a ciascun cliente, garantendo così la concorrenza del servizio.

Image

7.4. Un'applicazione con orologio grafico

Si consideri la seguente applicazione, che visualizza una finestra con un orologio e un pulsante per fermare o riavviare l'orologio:

Affinché l'orologio funzioni, un processo deve aggiornare l'ora ogni secondo. Allo stesso tempo, è necessario monitorare gli eventi che si verificano nella finestra: quando l'utente fa clic sul pulsante "Stop", l'orologio deve essere fermato. In questo caso abbiamo due attività parallele e asincrone: l'utente può fare clic in qualsiasi momento.

Consideriamo il momento in cui l'orologio non è ancora stato avviato e l'utente fa clic sul pulsante "Avvia". Si tratta di un evento classico e si potrebbe pensare che un metodo del thread in cui è in esecuzione la finestra possa quindi gestire l'orologio. Tuttavia, quando è in esecuzione un metodo dell'applicazione GUI, il suo thread non è più in ascolto degli eventi GUI. Questi eventi si verificano e vengono inseriti in una coda per essere elaborati dall'applicazione una volta terminato il metodo attualmente in esecuzione. Nel nostro esempio dell'orologio, il metodo sarà sempre in esecuzione poiché solo cliccando sul pulsante "Stop" è possibile fermarlo. Tuttavia, questo evento verrà elaborato solo una volta che il metodo avrà terminato. Siamo bloccati in un loop.

La soluzione a questo problema sarebbe che, quando l'utente fa clic sul pulsante "Start", venga avviata un'attività per gestire l'orologio, ma l'applicazione possa continuare ad ascoltare gli eventi che si verificano nella finestra. Avremmo quindi due attività separate in esecuzione in parallelo:

  • gestione dell'orologio
  • ascolto degli eventi della finestra

Torniamo al nostro orologio grafico:

No.
tipo
nome
ruolo
1
JTextField (Modificabile=false)
txtClock
visualizza l'ora
2
JButton
btnGoStop
ferma o avvia l'orologio

Il codice sorgente dell'applicazione realizzata con JBuilder è il seguente:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

public class interfaceHorloge extends JFrame {
  JPanel contentPane;
  JTextField txtHorloge = new JTextField();
  JButton btnGoStop = new JButton();

  // instance attributes
  boolean finHorloge=true;

   //Building the frame
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        System.exit(1);
      }//try
    }// while
  }// runHorloge

   //Initialize component
  private void jbInit() throws Exception  {
...................
  }

   //Replaced, so we can get out when the window is closed
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // start/stop clock
     // retrieve the button label
    String libellé=btnGoStop.getText();
    // launch?
    if(libellé.equals("Lancer")){
      // create the thread in which the clock will run
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//def thread
       // authorize the thread to start
      finHorloge=false;
       // change the button label
      btnGoStop.setText("Arrêter");
      // start the thread
      thHorloge.start();
      // end
      return;
    }//if
    // stop
    if(libellé.equals("Arrêter")){
      // the thread is told to stop
      finHorloge=true;
       // change the button label
      btnGoStop.setText("Lancer");
       // end
      return;
    }//if
  }
} 

Quando l'utente fa clic sul pulsante "Start", viene creato un thread utilizzando una classe anonima:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

Il metodo run del thread chiama il metodo runHorloge dell'applicazione. Una volta fatto ciò, il thread viene avviato:

      // on lance le thread
      thHorloge.start();

Verrà quindi eseguito il metodo runHorloge:

  private void runHorloge(){
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        System.exit(1);
      }//try
    }// while
  }// runHorloge

Il principio del metodo è il seguente:

  1. visualizza l'ora corrente nella casella di testo txtHorloge
  2. fa una pausa di 1 secondo
  3. riprende dal punto 1, dopo aver controllato la variabile booleana finHorloge, che verrà impostata su true quando l'utente clicca sul pulsante Stop.

7.5. Un'applet Orologio

Convertiamo l'applicazione grafica precedente in un'applet utilizzando il metodo standard e creiamo il seguente documento HTML, appletHorloge.htm:

<html>
  <head>
    <title>Applet Horloge</title>
  </head>
  <body>
      <h2>Une applet horloge</h2>
    <applet
      code="appletHorloge.class"
      width="150"
      height="130"
    ></applet>
    </center>
  </body>
</html>

Quando carichiamo questo documento direttamente in IE facendo doppio clic su di esso, otteniamo la seguente visualizzazione:

Image

In questo esempio, tutti gli elementi richiesti dall'applet si trovano nella stessa cartella:

E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002  12:17                3 174 appletHorloge.class
13/06/2002  12:17                  658 appletHorloge$1.class
13/06/2002  12:17                  512 appletHorloge$2.class
13/06/2002  12:20                  245 appletHorloge.htm

La nostra applet può essere migliorata. Abbiamo detto che quando l'applet viene caricata, viene eseguito il metodo init, seguito dal metodo start se esiste. Inoltre, quando l'utente lascia la pagina, viene eseguito il metodo stop se esiste. Quando l'utente torna alla pagina dell'applet, il metodo start viene richiamato nuovamente. Quando un'applet implementa thread di animazione visiva, i metodi start e stop dell'applet vengono spesso utilizzati per avviare e arrestare i thread. Infatti, non è necessario che un thread di animazione visiva continui a funzionare in background mentre l'animazione è nascosta.

Aggiungiamo quindi i seguenti metodi start e stop alla nostra applet:

  public void stop(){
    // the page is hidden
     // follow-up
    System.out.println("Page stop");
     // the page is hidden - stop the thread
    finHorloge=true;
  }

  public void start(){
     // the page reappears
     // follow-up
    System.out.println("Page start");
     // restart a new clock thread if necessary
    if(btnGoStop.getText().equals("Arrêter")){
      // change the wording
      btnGoStop.setText("Lancer");
       // and act as if the user had clicked on it
      btnGoStop_actionPerformed(null);
    }//if
  }//start

Inoltre, abbiamo aggiunto un controllo nel metodo run del thread per determinare quando inizia e quando si ferma:

  private void runHorloge(){
    // follow-up
    System.out.println("Thread horloge lancé");
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        return;
      }//try
    }// while
    // follow-up
    System.out.println("Thread horloge terminé");
  }// runHorloge

Ora eseguiamo l'applet con AppletViewer:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // applet launched - page displayed
Thread horloge lancé    // the thread is launched accordingly
Page stop    // iconized applet
Thread horloge terminé    // the thread is stopped accordingly
Page start    // applet redisplayed
Thread horloge lancé    // the thread is restarted
Thread horloge terminé    // press stop button
Thread horloge lancé    // press start button
Page stop    // icon applet
Thread horloge terminé    // thread arrested accordingly
Page start    // applet redisplay
Thread horloge lancé    // thread relaunched accordingly

Con AppletViewer, l'evento di avvio si verifica quando la finestra di AppletViewer è visibile, mentre l'evento di arresto si verifica quando viene minimizzata. I risultati sopra riportati mostrano che, quando il documento HTML viene nascosto, il thread viene effettivamente arrestato se era attivo.

7.6. Sincronizzazione delle attività

Nel nostro esempio precedente, c'erano due attività:

  • l'attività principale rappresentata dall'applicazione stessa
  • l'attività responsabile dell'orologio

Il coordinamento tra i due task era gestito dal task principale, che impostava un valore booleano per arrestare il thread dell'orologio. Affronteremo ora il problema dell'accesso concorrente da parte dei task alle risorse condivise, un problema noto anche come "condivisione delle risorse". Per illustrarlo, esamineremo prima un esempio.

7.6.1. Un contatore non sincronizzato

Consideriamo la seguente interfaccia grafica:

Image

No.
tipo
nome
ruolo
1
JTextField
txtAGenerate
specifica il numero di thread da generare
2
JTextField
(non modificabile)
txtGenerated
mostra il numero di thread generati
3
JTextField
(non modificabile)
txtStatus
fornisce informazioni sugli errori riscontrati e sull'applicazione stessa
4
JButton
btnGenerate
avvia la generazione del thread

L'applicazione funziona come segue:

  • l'utente specifica il numero di thread da generare nel campo 1
  • avvia la generazione di questi thread utilizzando il pulsante 4
  • i thread leggono il valore del campo 2, lo incrementano e visualizzano il nuovo valore. Inizialmente, questo campo contiene il valore 0.

I thread generati condividono una risorsa: il valore del campo 2. In questo caso, intendiamo illustrare i problemi che si incontrano in una situazione del genere. Ecco un esempio di esecuzione:

Image

Vediamo che abbiamo richiesto la generazione di 1.000 thread, ma ne sono stati contati solo 7. Il codice rilevante per l'applicazione è il seguente:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class interfaceSynchro extends JFrame {
  JPanel contentPane;
  JLabel jLabel1 = new JLabel();
  JTextField txtAGénérer = new JTextField();
  JButton btnGénérer = new JButton();
  JTextField txtStatus = new JTextField();
  JTextField txtGénérés = new JTextField();
  JLabel jLabel2 = new JLabel();

   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters

   //Building the frame
  public interfaceSynchro() {
..........
  }

   //Initialize component
  private void jbInit() throws Exception  {
......................
  }

   //Replaced, so we can get out when the window is closed
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //thread generation

     // read the number of threads to generate
    int nbThreads=0;
    try{
      // read the field containing the number of threads
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positive >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //error
      txtStatus.setText("Nombre invalide");
       // we start again
      txtAGénérer.requestFocus();
      return;
    }//catch

     // initially no threads generated
    txtGénérés.setText("0");  // task cpteur at 0

     // threads are generated and launched
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // create thread i
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//thread i
       // define its name
      tâches[i].setName(""+i);
      // start execution
      tâches[i].start();
    }//for
  }//generate

   // increment
  private void incrémente(){
     // retrieve the thread number
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // read the job counter value
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // increment it
    compteurs[iThread]++;

     // wait 100 milliseconds - the thread will then lose the processor
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     // the new counter is displayed
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    // follow-up
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// increment

}// class

Analizziamo il codice:

  • La finestra dichiara due variabili di istanza:
   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters

L'array tâches sarà l'array dei thread generati. L'array compteurs sarà associato all'array tâches. Ogni attività avrà il proprio contatore per recuperare il valore del campo txtGenerated dalla GUI.

  • Quando si fa clic sul pulsante Generate, viene eseguito il metodo btnGenerate_actionPerformed.
  • Questo metodo inizia recuperando il numero di thread da generare. Se necessario, viene segnalato un errore se questo numero non è valido. Genera quindi i thread richiesti, avendo cura di registrare i loro riferimenti in un array e assegnando un numero a ciascuno di essi. Il metodo run dei thread generati chiama il metodo increment della classe. Tutti i thread vengono avviati (start). Viene creato anche l'array dei contatori associati ai thread.
  • Il metodo increment:
  • legge il valore corrente del campo txtGénérés e lo memorizza nel contatore appartenente al thread attualmente in esecuzione
  • effettua una pausa di 100 ms per mettere intenzionalmente in inattività il processore
  • visualizza il nuovo valore nel campo txtGenerated

Spieghiamo ora perché il conteggio dei thread è errato. Supponiamo che ci siano 2 thread da generare. Essi si eseguono in un ordine imprevedibile. Uno di essi viene eseguito per primo e legge il valore 0 dal contatore. Lo imposta quindi su 1 ma non scrive " " nella finestra: si mette volontariamente in pausa per 100 ms. Perde quindi il processore, che viene assegnato a un altro thread. Questo thread opera allo stesso modo del precedente: legge il contatore della finestra e recupera lo 0 che è ancora presente. Imposta il contatore su 1 e, come il precedente, si mette in pausa per 100 ms. Il processore viene quindi riassegnato al primo thread: questo thread scrive il valore 1 nel contatore della finestra e termina. Il processore viene ora assegnato al secondo thread, che scrive anch'esso 1. Ci ritroviamo con un risultato errato.

Da dove deriva il problema? Il secondo thread ha letto un valore errato perché il primo thread è stato interrotto prima di aver completato il suo compito, che era quello di aggiornare il contatore nella finestra. Questo ci porta al concetto di risorsa critica e sezione critica in un programma:

  • Una risorsa critica è una risorsa che può essere detenuta da un solo thread alla volta. In questo caso, la risorsa critica è il contatore 2 nella finestra.
  • Una sezione critica di un programma è una sequenza di istruzioni nel flusso di esecuzione di un thread durante la quale esso accede a una risorsa critica. Dobbiamo assicurarci che durante questa sezione critica, esso sia l'unico ad avere accesso alla risorsa.

7.6.2. Conteggio sincronizzato per metodo

Nell'esempio precedente, ogni thread ha eseguito il metodo increment della finestra. Il metodo increment è stato dichiarato come segue:

    private void incremente()

Ora lo dichiariamo in modo diverso:

   // increment
  private synchronized void incrémente(){

La parola chiave **synchronized** significa che solo un thread alla volta può eseguire il metodo increment. Consideriamo la seguente notazione:

  • l'oggetto finestra F che crea thread in btnGenerate_actionPerformed
  • due thread T1 e T2 creati da F

Entrambi i thread vengono creati da F e quindi avviati. Di conseguenza, entrambi eseguiranno il metodo F.run. Supponiamo che T1 arrivi per primo. Esegue F.run e poi F.increment, che è un metodo sincronizzato. Legge il valore del contatore 0, lo incrementa e poi si mette in pausa per 100 ms. Il processore viene quindi ceduto a T2, che a sua volta esegue F.run e poi F.increment. A questo punto, viene bloccato perché il thread T1 sta attualmente eseguendo F.increment, e la parola chiave synchronized garantisce che solo un thread alla volta possa eseguire F.increment. T2 perde quindi il processore senza essere riuscito a leggere il valore del contatore. Dopo 100 ms, T1 riacquista il processore, visualizza il valore del contatore 1, esce da F.increment e poi da F.run, e termina. T2 riacquista quindi il processore e ora può eseguire F.incremente poiché T1 non sta più eseguendo questo metodo. T2 legge quindi il valore del contatore 1, lo incrementa e si mette in pausa per 100 ms. Dopo 100 ms, riacquista il processore, visualizza il valore del contatore 2 e termina a sua volta. Questa volta, il valore ottenuto è corretto. Ecco un esempio testato:

Image

7.6.3. Conteggio sincronizzato da un oggetto

Nell'esempio precedente, l'accesso al contatore txtGénérés era sincronizzato da un metodo. Se la finestra che crea i thread si chiama F, possiamo anche dire che il metodo F.incremente rappresenta una risorsa che dovrebbe essere utilizzata da un solo thread alla volta. Si tratta quindi di una risorsa critica. L'accesso sincronizzato a questa risorsa era garantito dalla parola chiave synchronized:

    private synchronized void incrémente()

Si potrebbe anche dire che la risorsa critica è l'oggetto F stesso. Si tratta di una condizione più restrittiva rispetto al caso in cui la risorsa critica sia F.increment. Infatti, in quest'ultimo caso, se un thread T1 esegue F.increment, un thread T2 non può eseguire F.increment ma può eseguire un altro metodo dell'oggetto F, indipendentemente dal fatto che sia sincronizzato o meno. Nel caso in cui l'oggetto F stesso sia la risorsa critica, quando un thread T1 esegue una sezione sincronizzata di questo oggetto, ogni altra sezione sincronizzata dell'oggetto diventa inaccessibile agli altri thread. Pertanto, se il thread T1 esegue il metodo sincronizzato F.increment, il thread T2 non sarà in grado di eseguire non solo F.increment ma anche qualsiasi altra sezione sincronizzata di F, anche se nessun altro thread la sta utilizzando. Si tratta quindi di un metodo più restrittivo.

Supponiamo, quindi, che la finestra diventi la risorsa critica. Scriveremmo quindi:

   // increment
  private void incrémente(){
     // review section
    synchronized(this){
       // retrieve the thread number
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       // read the job counter value
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // increment it
      compteurs[iThread]++;

       // wait 100 milliseconds - the thread will then lose the processor
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       // the new counter is displayed
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//synchronized
  }// increment

Tutti i thread utilizzano la finestra this per la sincronizzazione. In fase di esecuzione, otteniamo gli stessi risultati corretti di prima. In realtà possiamo sincronizzarci su qualsiasi oggetto noto a tutti i thread. Ecco un altro metodo, ad esempio, che fornisce gli stessi risultati:

   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters
    Object synchro=new Object(); // a thread synchronization object

   // increment
  private void incrémente(){
     // review section
    synchronized(synchro){
..............
    }//synchronized
  }// increment

La finestra crea un oggetto di tipo Object che verrà utilizzato per la sincronizzazione dei thread. Questo metodo è migliore di quello che sincronizza sull'oggetto this perché è meno restrittivo. In questo caso, se un thread T1 si trova nella sezione sincronizzata di increment e un thread T2 vuole eseguire un'altra sezione sincronizzata dello stesso oggetto this ma sincronizzata da un oggetto diverso da synchro, potrà farlo.

7.6.4. Sincronizzazione basata sugli eventi

In questo caso, utilizziamo una variabile booleana canPass per indicare a un thread se può o meno entrare in una sezione critica. Una versione senza sincronizzazione potrebbe apparire così:


while(! peutPasser);        // on attend que peutPasser passe à vrai
peutPasser=false;            // aucun autre thread ne doit passer
section critique;            // ici le thread est tout seul
peutPasser=true;            // un autre thread peut passer dans la section critique

La prima istruzione, in cui un thread esegue un ciclo in attesa che canPass diventi vero, è inefficiente: il thread occupa inutilmente il processore. Questo fenomeno è chiamato attesa attiva. Possiamo migliorare il codice come segue:

while(! peutPasser){        // wait for peutPasser to change to true
   Thread.sleep(100);    // off for 100 ms
}
peutPasser=false;            // no other thread may pass
section critique;            // here the thread is all alone
peutPasser=true;            // another thread can enter the critical section

Il ciclo di attesa è migliore in questo caso: se il thread non può passare, rimane inattivo per 100 ms prima di verificare nuovamente se può passare o meno. Nel frattempo, il processore verrà assegnato ad altri thread nel sistema.

Entrambi questi metodi sono in realtà errati: non impediscono a due thread di entrare nella sezione critica contemporaneamente. Supponiamo che un thread T1 rilevi che canPass è vero. Procederà quindi all'istruzione successiva dove imposta canPass su falso per bloccare gli altri thread. Tuttavia, potrebbe benissimo essere preemptato in quel momento, sia perché la sua porzione di tempo di processore è scaduta, sia perché un'attività con priorità più alta ha richiesto il processore, o per qualche altra ragione. Il risultato è che perde il processore. Lo riavrà poco dopo. Nel frattempo, altri task otterranno il processore, tra cui forse un thread T2 che esegue un ciclo in attesa che peutPasser diventi vero. Anche questo scoprirà che peutPasser è vero (il primo thread non ha avuto il tempo di impostarlo su falso) ed entrerà anch'esso nella sezione critica. Cosa che non avrebbe dovuto accadere.

La sequenza


while(! peutPasser){            // wait for peutPasser to change to true
   try{
        Thread.sleep(100);    // off for 100 ms
    } catch (Exception e) {}
}// while
peutPasser=false;                // no other thread may pass

è una sezione critica che deve essere protetta dalla sincronizzazione. Basandoci sull'esempio precedente, possiamo scrivere:


    synchronized(synchro){
        while(! peutPasser){            // wait for peutPasser to change to true
            try{
                Thread.sleep(100);    // off for 100 ms
            } catch (Exception e) {}
        }//while
        peutPasser=false;                // no other thread may pass
    }// synchronized
    section critique;                    // here the thread is all alone
    peutPasser=true;                    // another thread can enter the critical section

Questo esempio funziona correttamente. Possiamo migliorarlo evitando l'attesa semi-attiva del thread mentre controlla regolarmente il valore del booleano canPass. Invece di svegliarsi ogni 100 ms per controllare lo stato di canPass, può andare in standby e richiedere di essere svegliato quando canPass è vero. Scriviamo questo come segue:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // if we can't get through then we wait
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // no other thread may pass
}// synchronized

L'operazione synchro.wait() può essere eseguita solo da un thread che sia l'attuale "proprietario" dell'oggetto synchro. In questo caso, si tratta della sequenza:


synchronized(synchro){

}// synchronized

che garantisce che il thread possieda l'oggetto synchro. Attraverso l'operazione synchro.wait(), il thread cede la proprietà del blocco di sincronizzazione. Perché? Generalmente perché non dispone delle risorse necessarie per continuare a lavorare. Quindi, piuttosto che bloccare altri thread in attesa della risorsa synchro, la cede e attende la risorsa di cui è sprovvisto. Nel nostro esempio, attende che il valore booleano canPass diventi true. Come verrà notificato di questo evento? Come segue:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // if we can't get through then we wait
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // no other thread may pass
}// synchronized
section critique...
synchronized(synchro){
      synchro.notify();
    }

Consideriamo il primo thread che acquisisce il blocco di sincronizzazione. Chiamiamolo T1. Supponiamo che trovi il valore booleano canPass vero, poiché è il primo thread. Quindi lo imposta su falso. Quindi esce dalla sezione critica bloccata dall'oggetto di sincronizzazione. Un altro thread può quindi entrare nella sezione critica per verificare il valore di canPass. Lo troverà falso e attenderà quindi un evento (wait). In questo modo, rinuncia alla proprietà dell'oggetto sync. Un altro thread può quindi entrare nella sezione critica: anche questo attenderà perché canPass è falso. Possiamo quindi avere più thread in attesa di un evento sull'oggetto sync.

Torniamo al thread T1, al quale è stato concesso l'accesso. Esso esegue la sezione critica e poi segnala che un altro thread può ora procedere. Lo fa con la seguente sequenza:


synchronized(synchro){
      synchro.notify();
    }

Deve prima riottenere il possesso dell'oggetto sync utilizzando l'istruzione synchronized. Questo non dovrebbe essere un problema poiché è in competizione con thread che, se ottengono momentaneamente l'oggetto sync, devono rilasciarlo tramite un wait poiché trovano peutPasser falso. Quindi il nostro thread T1 acquisirà alla fine la proprietà dell'oggetto synchro. Una volta fatto ciò, segnala tramite l'operazione synchro.notify che uno dei thread bloccati da un synchro.wait deve essere riattivato. Cede quindi nuovamente la proprietà dell'oggetto sync, che viene poi assegnato a uno dei thread in attesa. Questo thread continua la sua esecuzione con l'istruzione che segue il wait che lo aveva messo in attesa. A sua volta, eseguirà la sezione critica ed emetterà un synchro.notify per rilasciare un altro thread. E così via.

Vediamo come funziona utilizzando l'esempio di conteggio che abbiamo già studiato.

  void btnGénérer_actionPerformed(ActionEvent e) {
     //thread generation

     // read the number of threads to generate
    int nbThreads=0;
    try{
      // read the field containing the number of threads
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positive >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //error
      txtStatus.setText("Nombre invalide");
       // we start again
      txtAGénérer.requestFocus();
      return;
    }//catch

     // RAZ job counter
    txtGénérés.setText("0");  // task cpteur at 0
    // 1st thread can pass
    peutPasser=true;
     // threads are generated and launched
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // create thread i
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//thread i
       // define its name
      tâches[i].setName(""+i);
      // start execution
      tâches[i].start();
    }//for
  }//generate

Ora, i thread non eseguono più il metodo increment, ma il seguente metodo synchronize:

   // thread synchronization step
  public void synchronise(){
     // request access to the critical section
    synchronized(synchro){
      try{
        // can we get through?
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         // we have passed - we forbid other threads to pass
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//try
    }// synchronized

    // review section
    System.out.println(Thread.currentThread().getName()+ " passé");
    incrémente();

     // we're done - we release any thread blocked at the entrance to the critical section
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }// synchronized
  } // synchronize

Lo scopo del metodo synchronize è quello di elaborare i thread uno alla volta. Per farlo, utilizza una variabile di sincronizzazione chiamata synchro. Il metodo increment non è più protetto dalla parola chiave synchronized:

   // increment
  private void incrémente(){
     // retrieve the thread number
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // read the job counter value
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // increment it
    compteurs[iThread]++;
     // wait 100 milliseconds - the thread will then lose the processor
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     // the new counter is displayed
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }// increment

Per 5 thread, i risultati sono i seguenti:

0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé

Siano T0 a T4 i cinque thread generati dall'applicazione. T0 è il primo ad acquisire il blocco di sincronizzazione e trova peutPasser impostato su true. Imposta peutPasser su false e procede: questo è lo scopo del primo messaggio inviato. Con ogni probabilità, continua ed esegue la sezione critica, in particolare il metodo increment. In questo metodo, entrerà in stato di sospensione per 100 ms (sleep). Rilascia quindi il processore. Il processore viene quindi assegnato a un altro thread, il thread T1, che acquisisce la proprietà dell'oggetto di sincronizzazione. Scopre di non poter procedere ed entra in uno stato di attesa (wait). Rilascia quindi la proprietà dell'oggetto di sincronizzazione e del processore. Il processore viene assegnato al thread T2, che subisce la stessa sorte. Durante i 100 ms in cui T0 è in pausa, i thread da T1 a T4 vengono quindi messi in attesa. Questo è il significato dei 4 messaggi "waiting". Dopo 100 ms, T0 riacquista il processore e completa il proprio lavoro: questo è il significato del messaggio "0 finished". Rilascia quindi uno dei thread bloccati e termina. Il processore liberato viene quindi assegnato a un thread disponibile: quello appena rilasciato. In questo caso, è T1. Il thread T1 entra quindi nella sezione critica: questo è il significato del messaggio "1 passed". Esegue ciò che deve fare e poi si mette in pausa per 100 ms. Il processore è quindi disponibile per un altro thread, ma tutti sono in attesa di un evento: nessuno di essi può prendere il processore. Dopo 100 ms, il thread T1 riprende il processore e termina: questo è il significato del messaggio "1 finished". I thread da T1 a T4 si comporteranno allo stesso modo di T1: questo è il significato delle tre serie di messaggi: "passed", "finished".