Nei mesi
scorsi abbiamo introdotto i concetti fondamentali di programmazione concorrente
e descritto i costrutti classici di semaforo e monitor. Abbiamo sfruttato
la semplicità ed il supporto al multithreading di Java per studiare
i problemi di blocco critico ed individuale cercando di dare soluzioni
di carattere generale. Dopo molti concetti teorici, questo mese vediamo
quali sono i processi sempre presenti in un applet o in un'applicazione
Java, concludendo con un esempio pratico in cui risalti tutta la potenza
e semplicità di programmazione dei Thread Java.
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 time_out, 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 time_out 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.
|
MokaByte Web
1998 - www.mokabyte.it
MokaByte ricerca
nuovi collaboratori. Chi volesse mettersi in contatto con noi può
farlo scrivendo a mokainfo@mokabyte.it
|
|