I Thread in una applicazione Java

Sappiamo che quando viene eseguita una applicazione Java, esiste un thread di default che esegue il metodo main(). In realtà i thread sono più di uno e si possono raggruppare in:

> System Thread Group (Gruppo dei thread di sistema)

> Main Group (Gruppo del thread principale)

Il System Thread Group dispone di quattro thread demoni che forniscono alcune delle funzionalità descritte nelle specifiche JVM (Java Virtual Machine). Questi thread sono:

  • Clock Handler: Gestisce tutti gli eventi legati in qualche modo a grandezze temporali. Nel caso della chiamata al metodo Thread.sleep(), per esempio, permette di misurare l'intervallo di attesa. Vedremo meglio questo thread quando parleremo degli applet.
  • Idle Thread: Questo thread è a priorità bassissima; è un processo interno alla JVM e si può considerare di priorità 0. In questo modo esso sarà attivo quando tutti gli altri thread sono bloccati. L'utilizzo principale di questo processo è quello di avvisare il garbage collector del momento opportuno per agire.
  • Garbage Collector: Il garbage collector scandisce gli oggetti presenti nella JVM, verifica il loro utilizzo e li elimina se non più referenziati. Questo processo resta in attesa per un secondo e quando si sveglia verifica se l'Idle Thread è in esecuzione. Ciò significa che il GC può agire in quanto il sistema non è in azione e quindi inizia a scandire la memoria. Se il processo Idle non è attivo, significa che esiste un qualche altro processo in esecuzione per cui il GC attende per un altro secondo\[7\].
  • Finalizer Thread: Questo thread ha priorità 1 e viene attivato dal GC quando si incontra un oggetto non più referenziato. Esso permette l'esecuzione del metodo finalize().

Il Main Group contiene, inizialmente, il solo:

  • Main Thread: È il thread di default che esegue il metodo main() di una applicazione.

Non appena una applicazione Java crea una finestra per la gestione di una certa interfaccia grafica, vengono creati altri thread che vengono aggiunti al gruppo del thread principale e permettono di gestire gli eventi legati alla gestione del sistema delle finestre. Essi sono:

  • AWT-Input Thread: Gestisce gli input dal sistema di gestione a finestre del sistema operativo e le traduce in chiamate agli opportuni metodi AWT.
  • AWT-Toolkit: Gestisce gli eventi dal sistema di gestione a finestre del sistema operativo traducendole in opportune chiamate a metodi AWT. Questo thread è il responsabile, per esempio, delle chiamate ai metodi handleEvent() o action() nella gestione degli eventi con il JDK1.02. Questo thread è dipendente dalla piattaforma e si chiama, per esempio, AWT-Motif in UNIX, AWT-Windows per Windows95, ecc..
  • Screen Updater: Gestisce le chiamate al metodo repaint(). Quando ci sono chiamate al metodo repaint(), viene chiamato il metodo notify() su questo thread che poi si preoccupa di invocare il metodo update() relativo a tutti i componenti interessati.

I gruppi di thread sono legati tra loro da una relazione gerarchica ad albero. La radice di questo albero è proprio il System Thread Group mentre il Main Group è suo figlio.

> Che cosa sono i gruppi di thread > > Ma cos’è un gruppo di thread? Senza entrare nel dettaglio, supponiamo di avere un applet in cui esistono diversi > processi in esecuzione. Possiamo avere, per esempio, un processo che carica delle immagini, uno che legge da una connessione socket verso il server, ecc. Nel caso in cui l’utente cambiasse pagina, il browser chiama il metodo stop() dell’applet. Bisognerebbe, quindi, ridefinire lo stesso metodo in modo da chiamare il metodo stop() di ciascun processo. > L’oggetto java.util.ThreadGroup ci viene in aiuto in quanto ci permette di unire i vari thread in un unico gruppo. Per fermare tutti i thread del gruppo, basterà chiamare il metodo stop() dell’oggetto ThreadGroup. I gruppi di thread non sono solo insiemi di thread ma permettono di organizzare i thread secondo una struttura ad albero. Ogni gruppo di thread, tranne quello principale (root) dispone di un gruppo genitore. Questa organizzazione è alla base del meccanismo di sicurezza negli applet gestito, come sappiamo, dal SecurityManager. Tutti i thread di un applet appartengono ad uno stesso gruppo e nel SecurityManager esistono metodi che permettono di impedire a thread di un gruppo di accedere a quelli di un altro.

>

I Thread in un applet

I thread che abbiamo descritto per le applicazioni esistono anche negli applet. Quando il browser scarica un applet, crea un altro gruppo di thread che diviene figlio del Main Thread. In seguito, dentro il gruppo di thread creato, il browser crea un nuovo thread responsabile dell'esecuzione dell'applet.

Prima di passare alla descrizione dell'utilizzo dei thread in Java per i problemi più comuni, diamo una dimostrazione della esistenza dei thread appena descritti. Supponiamo di creare un applet che permette di scrivere una particolare stringa dopo un conteggio alla rovescia.

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*       Applet Esempio1
\*       Questo applet dimostra la presenza del thread
\*       dell'applet creato dal browser, del Clock Handler
\*       e dello ScreenUpdater.
\*
\*@param author: Massimo Carli
\*@paran date    : 04/04/1998
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
import  java.applet.\*;
import  java.awt.\*; 
public class Esempio1 extends Applet {
      protected int num;
      public void init(){
         num=10;
      }// fine init
      public void paint(Graphics g){
         if (num==0){
           // Se num=0 abbiamo finito il conteggio per cui
           // visualizziamo il messaggio
           g.drawString("PARTITO!! ",10,20); 
           // Rimettiamo il conteggio a 10
           num=10;
         }
         else {
           // Altrimenti scriviamo il valore raggiunto
           g.drawString((num-)+"",10,20);
         }
         // Facciamo la prossima repaint dopo 1 secondo
         // ovvero 1000 millisecondi
         repaint(1000);
      }// fine paint 
      public boolean mouseDown(Event e, int x, int y){
      System.out.println("Premuto "+e);
      return false;
   }
  }// fine Esempio1

Listato 1: Esistenza del Clock Handler e dello ScreenUpdater

L'applet è molto semplice e consiste nella esecuzione della repaint() ripetuta ogni secondo.

Per eseguire una repaint() dopo un certo numero (delay) di millisecondi, si utilizza il metodo paint(int delay) della classe java.awt.Component. Se esistesse un unico thread nel periodo di attesa tra una repaint() e la successiva, l'applet sarebbe bloccato per cui certi eventi del mouse, per esempio, non sarebbero sentiti che dopo un secondo.

Il fatto che si possa attendere un certo intervallo dimostra la presenza del Clock Handler; il metodo repaint() chiede al Clock Handler di svegliarlo dopo un secondo; a quel punto, lo stesso Clock Handler, si dovrà preoccupare di far chiamare il metodo update(). Questo significa che il Clock Handler deve coordinarsi anche con lo Screen Updater che chiamerà il metodo update() quando il Clock Handler glielo dirà. Intanto il thread creato dal browser per l'applet continuerà la sua esecuzione facendo ritornare il metodo paint(). Il fatto che gli eventi del mouse vengano gestiti dimostra, inoltre, l'esistenza del thread AWT-Toolkit.

Un esempio pratico

Per dimostrare quanto utili possano essere i Thread nella programmazione Java, facciamo un esempio pratico.

Creiamo un applet che permette di visualizzare un insieme di messaggi in modo scorrevole. I messaggi sono letti da server e risiedono in un file testo. L'applet, non appena caricato, dovrà leggere il file testo contenente i messaggi e, se il tutto ha funzionato correttamente, dovrà iniziare a visualizzarli uno dopo l'altro. Durante la lettura del file di testo, l'applet dovrà visualizzare un messaggio di loading ed eventualmente avvisare se si è presentato qualche problema come la mancanza del file di testo. Come ultima specifica, facciamo in modo che se il contenuto del file testo non è stato caricato entro 1 secondo, venga visualizzato un messaggio di errore. Il problema si articola quindi, nelle seguenti fasi:

1\) Caricamento dell'applet e visualizzazione del messaggio di loading

2\) Lettura del file con partenza del timer per il time-out di caricamento

3\) Comunicazione dell'avvenuto caricamento con passaggio del contenuto del file testo

4\) Avvio della visualizzazione.

Da una veloce analisi del problema risalta la necessità di utilizzare processi diversi per assolvere funzioni parallele diverse. Realizziamo quindi una classe Timer (Listato 2) che permette di attendere un determinato periodo di tempo, scaduto il quale verrà chiamato un particolare metodo degli oggetti interessati.

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\* Classe Timer
\* Questa classe rappresenta un Timer che, se il flag repeat
\* è true, richiama il metodo tic() di ogni suo ascoltatore
\* ogni delay millisecondi. Se il flag repeat è false, il metodo
\* tic sarà chiamato solo una volta.
\*@param author: Massimo Carli
\*@paran date    : 04/04/1998
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
import  java.util.Vector;
public class Timer extends Thread  { 
// Numero di millisecondi da attendere
private  int delay; 
// Indica se il metodo tic() deve essere richiamato
// periodicamente (true) o no (false)
private boolean repeat; 
// Insieme degli ascoltatori del Timer 
private Vector  ascoltatori; 
/\*\*
\* Costruttore vuoto.
\*/
public Timer(){
    this(1000,false);
}// fine costruttore 
/\*\*
\*Costruttore con ritardo
\*@param delay : ritardo
\*/
public Timer(int delay){
    this(delay,false);
}// fine costruttore
/\*\*
\*Costruttore completo
\*@param delay   : ritardo
\*@param repeat  : se true il metodo tic() viene chiamato ripetutamente
\*/
public Timer(int delay,boolean repeat){
   super("Timer");   // Creiamo un Thread di nome Timer
   this.delay=delay; // Riferimento al ritardo in millisecondi
   this.repeat=repeat;   // Riferimento
   ascoltatori = new Vector(); // Creiamo l'insieme degli ascoltatori
}// fine costruttore
// Corpo del thread
public void run(){
   while(true){
      try{
         Thread.sleep(delay);
      }catch(InterruptedException e){}
      notifica();
      if (!repeat) return;
   }// fine while
}// fine run
// Questo metodo richiama il metodo tic() di
// ciascun ascoltatore
private final void notifica(){
  Vector copia= new Vector();
  synchronized(ascoltatori){
    copia=(Vector)(ascoltatori.clone());
  }
for (int i=0;i
......

Listato 2: Class Timer

Per ottenere questo effetto ci serviamo dell'interfaccia TimerListener (Listato 3).

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*       Interface TimerListener  \*                                                                                                                                                                      \*
\*       Questa interfaccia sarà  implementata   da ogni oggetto
\*       ascoltatore del Timer. Il Timer richiamerà il metodo tic()
\*       di   ogni oggetto ascoltatore.
\*  \*                                                                                                                                                                       \*@param author: Massimo Carli
\*@paran date    :04/04/1998
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
public interface TimerListener  {
          /\*\*
          \* Il Timer richiamerà il metodo tic di ogni oggetto
          \* ascoltatore che implementa questa interfaccia
          \*/ 
          public void tic();  
  }// fine interface

Listato 3: Interfaccia TimeListener

Essa prevede semplicemente la descrizione del metodo tic() che verrà chiamato non appena il tempo di timeout, settato nel costruttore di Timer, sarà scaduto. La classe Timer, alla luce di quanto visto nei mesi scorsi, è molto semplice. Essa estende la classe Thread e definisce il metodo run() in modo tale da attendere il numero di millisecondi specificati nella variabile delay. Per la sospensione del Thread utilizziamo il metodo sleep(), che abbiamo scoperto essere regolato dal Clock Handler. Se un oggetto è interessato allo scadere del timeout dovrà semplicemente implementare l'interfaccia TimerListener e registrarsi ad esso chiamando il suo metodo addTimerListener().

Un comportamento analogo è svolto dalla classi Loader (Listato 4)

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
 \*       Classe  Loader
 \*       Questa classe permette di caricare un le righe di un
 \*       file testo dato il suo URL.     Il contenuto delle righe sarà
 \*       contenuto in un array di stringhe.
 \*@param author: Massimo Carli
 \*@paran date    : 04/04/1998
 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ 
  import   java.util.Vector;
  import   java.io.\*;
  import   java.net.\*; 
 public class Loader extends Thread  { 
        // URL da cui scaricare il contenuto del file
        private  URL   url; 
        // Insieme degli ascoltatori del Timer
        private  Vector          ascoltatori; 
          /\*\*
          \*Costruttore con ritardo
          \*@param delay : ritardo
          \*/ 
          public Loader(URL url){
                  super("Loader");        // Creiamo il thread di nome "Loader"
                  this.url=url;
                  ascoltatori= new Vector();
          }// fine costruttore 
          // Corpo del thread
          public void run(){
                  try{
                          InputStream in= url.openStream();
                          DataInputStream din= new DataInputStream(in);
                          Vector dati= new Vector();
                          while(din.available()\>0){
                                  dati.addElement(din.readLine());
                          }
                          String\[\] ret=new String\[dati.size()\];
                          .........

Listato 4: Class Loader

e dall'interfaccia LoaderListener (Listato 5).

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*       Interface LoaderListener
\*       Questa interfaccia sarà  implementata da ogni oggetto
\*       ascoltatore del Loader.  Non  appena il loader ha caricato
\*       le informazioni volute, chiamerà il metodo loaded() degli
\*       ascoltatori passando un codice esplicativo dell'esito,
\*       ed uno caratteristico della struttura caricata.
\* 
\*@param author: Massimo Carli
\*@paran date    : 04/04/1998  
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
public interface LoaderListener  {
        /\*\*
        \* Caricamento avvenuto in modo corretto
        \*/
        public  int             OK=0;
        /\*\*
        \* Caricamento avvenuto in modo errato
        \*/
        public  int             ERROR=1; 
       /\*\*
       \* Il Loader richiamerà il metodo loaded di ogni oggetto
       \* ascoltatore che implementa questa interfaccia
       \*/
       public void loaded(int esito,Object obj);
}// fine interface

Listato 5: Interfaccia LoaderListener

La differenza consiste semplicemente nel corpo del thread che, in questo caso, legge da uno stream ottenuto dall'url che identifica il file testo contenente le stringhe da visualizzare. Quando il file testo è stato completamente letto, viene chiamato il metodo loaded() di ciascun ascoltatore di Loader e verranno passate le informazioni caricate insieme all'esito del caricamento stesso. Veniamo, allora, alla classe Scroller (Listato 6) che rappresenta l'applet per lo scrolling. Esso deve essere ascoltatore sia di Timer che di Loader per cui implementerà le interfacce TimerListener e LoaderListener rispettivamente. Il metodo alla base del funzionamento dell'applet è il metodo start() e si preoccuperà di creare ed avviare il timer ed il loader. A questo punto si tratterà di attendere quale tra i metodi tic() e loaded() verrà chiamato per primo. Nel caso del metodo tic() verrà visualizzato un errore in quanto il tempo di time_out è scaduto prima dell'avvenuto caricamento dei dati. Nel caso del metodo loaded() significa che l'insieme dei dati è a nostra disposizione per cui si potrà finalmente avviare il thread dell'applet. Ma per quale motivo sono necessari tutti questi thread?

Per quello che riguarda il thread della classe Timer possiamo dire di aver creato un thread simile al Clock Handler con l'importante differenza di poterlo facilmente controllare con i metodi classici della classe Thread (start(), stop(), ecc.). Per quello che riguarda la classe Loader la necessità dell'utilizzo di un Thread è dovuta alla natura bloccante della lettura da stream in Java. Possiamo pensare ad uno stream in Java come ad un canale da cui estrarre (nel caso di stream di lettura) un certo insieme di dati. Se si prova ad estrarre un dato da uno stream che è momentaneamente vuoto, il processo responsabile della lettura si blocca fino all'arrivo di nuovi dati.

È chiaro che se il processo che legge dallo stream è l'unico processo presente, si ha il blocco dell'intera applicazione o applet. Attraverso l'utilizzo dei thread si fa in modo che il processo in lettura attenda la disponibilità di dati che un eventuale altro thread gli potrà fornire.

Conclusioni

Java offre un supporto multithreading molto interessante anche se è, questo, un argomento intrinsecamente spinoso e difficile da capire. Durante questo mini corso sono stati affrontati gli argomenti in modo da dare una buona conoscenza di base, che potrete espandere con i numerosi ed interessanti testi sull'argomento.

Bibliografia

  • - \
  • [1\] *"Principi e tecniche di programmazione concorrente"*, ed. UTET Paolo Ancillotti, Maurelio Boari.
  • - \
  • [2\] *"Concurrent Programming in Java-Design and Pattern"*, Doug Lea, Addison Wedsley.
  • - \
  • [3\] *"Java in a NutShell"*, 2nd Edition David Flanagan, O'Reilly.
  • - \
  • [4\] *"Java Threads"*, Scott Oaks, Henry Wong, O'Reilly.
  • - \
  • [5\] *"Corso di elaborazione delle immagini in Java"*, M. Carli - Mokabyte 2 e successivi.
  • - \
  • [6\] *"Tha Java Language Specification"*, JavaSoft.
  • - \
  • [7\] *"Il Garbage Collector di Java"*, Computer Programming N°66 Feb.98 Ed. Infomedia.