I thread, o processi leggeri, sono flussi di esecuzione all’interno di un processo in corso.
In altre parole, un processo può essere suddiviso in vari thread, ciascuno dei quali rappresenta un flusso di esecuzione indipendente dagli altri.
I thread appartenenti allo stesso processo condividono quasi tutte le risorse, come la memoria e i file aperti, tranne:
Il program counter e lo stack sono proprio quelle risorse che consentono ad un thread di avere un flusso di esecuzione indipendente.
Java è l’unico tra i linguaggi di programmazione maggiormente utilizzati ad offrire un supporto nativo ai thread.
Ad esempio, per supportare i thread il linguaggio C/C++ necessita di librerie esterne, spesso fornite dal sistema operativo (come la libreria per i thread POSIX).
Siccome la virtual machine di Java funge anche da sistema operativo per i programmi Java, essa offre in maniera nativa il supporto ai thread.
Esamineremo due modi alternativi di creare un thread.
Entrambi i modi sono supportati dalla classe Thread.
Per evitare confusione tra i thread e gli oggetti della classe Thread, chiameremo “thread di esecuzione” i primi e “oggetti thread” i secondi.
In Java, ad ogni thread di esecuzione è associato un oggetto thread, mentre il viceversa non è sempre vero.
Una applicazione Java termina quando tutti i suoi thread sono terminati.
Ogni applicazione Java parte con almeno un thread, detto thread principale (main thread), che esegue il metodo main della classe di partenza.
Anche al thread principale è associato, in maniera automatica, un oggetto thread; in seguito vedremo come ottenere un riferimento a questo oggetto.
Il primo modo di creare un thread di esecuzione consiste nei seguenti passi:
1. creare una classe X che estenda Thread;
2. affinché il nuovo thread di esecuzione faccia qualcosa, la classe X deve effettuare l’overriding del metodo “run”, la cui intestazione in Thread è semplicemente
public void run()
3. istanziare la classe X;
4. chiamare il metodo start dell’oggetto creato.
Naturalmente, all’occorrenza il procedimento può essere semplificato utilizzando una classe X anonima.
Ad esempio, creiamo un thread di esecuzione che stampa i numeri da 0 a 9, con una pausa di un secondo tra un numero e il successivo. A questo scopo, creiamo una classe che estende Thread, il cui metodo run svolge il compito prefissato
public class MyThread extends Thread {
public void run() {
for (int i=0; i<10 ;i++) {
System.out.println(i);
try {
Thread.sleep(1000);
catch (InterruptedException e) {
return;
}
}
}
}
A questo punto, istanziamo la classe MyThread e facciamo partire il corrispondente thread di esecuzione.
MyThread t = new MyThread();
t.start()
Non abbiamo dotato la classe MyThread di un costruttore in quanto Thread ha un costruttore senza argomenti, che è sufficiente per i nostri scopi.
Osserviamo che prima di chiamare il metodo start, c’è un oggetto di tipo Thread a cui non corrisponde (ancora) nessun thread di esecuzione.
L’intestazione di start in Thread è semplicemente
public void start()
La chiamata a start non è bloccante; per definizione, il nuovo thread di esecuzione svolge le sue operazioni in parallelo al resto del programma.
Il nuovo thread di esecuzione esegue automaticamente il metodo run dell’oggetto thread corrispondente.
Il metodo run viene anche detto l’entry point del thread, perché è il primo metodo che viene eseguito.
In questo senso, il metodo main è l’entry point del thread principale.
Quando il metodo run termina, anche il thread di esecuzione termina.
Non è consentito chiamare start più di una volta sullo stesso oggetto thread, anche se la prima esecuzione del thread è terminata.
Nell’esempio abbiamo usato anche un’altro metodo della classe Thread:
public static void sleep(long millis) throws InterruptedException
Tale metodo statico mette in attesa il thread corrente (cioè, quello che chiama sleep) per un dato numero di millisecondi.
Se l’attesa viene interrotta, e vedremo in seguito come, il metodo lancia l’eccezione verificata InterruptedException.
Questa è una caratteristica comune a tutti i metodi cosiddetti “bloccanti”, che cioè possono mettere in attesa un thread.
Domanda: perché secondo voi è stato deciso che l’eccezione lanciata fosse verificata?
Quando si cattura l’eccezione InterruptedException, è buona norma terminare il thread di esecuzione corrente (si veda la slide “La disciplina delle interruzioni”).
Se ci si trova nel metodo run di un oggetto thread, per terminare il thread corrente è sufficiente utilizzare “return”.
Esaminiamo altri metodi utili della classe Thread
public static Thread currentThread()
Restituisce l’oggetto thread corrispondente al thread di esecuzione che l’ha invocato.
Con questo metodo è possibile anche ottenere un riferimento all’oggetto thread corrispondente al thread principale (quello che esegue inizialmente il metodo main)
public final void join() throws InterruptedException
Il metodo join interagisce con due thread (sia oggetti, sia thread di esecuzione):
Il metodo join mette in attesa il thread 1 fino alla terminazione del thread 2.
Se il thread 2 è già terminato, il metodo join ritorna immediatamente.
Pertanto, si tratta di un metodo bloccante.
Come tutti i metodi bloccanti, lancia l’eccezione verificata InterruptedException se l’attesa viene interrotta.
Questo metodo svolge lo stesso compito della system call waitpid() dei sistemi Unix, nonché della funzione pthread_join() dello standard POSIX thread.
È spesso utile interrompere le operazioni di un thread.
A tale scopo, ogni thread è dotato di un flag booleano chiamato stato di interruzione, inizialmente falso.
I metodi bloccanti, come sleep e join, vengono interrotti non appena lo stato di interruzione diventa vero.
Il seguente metodo della classe Thread imposta a vero lo stato di interruzione del thread sul quale è chiamato
public void interrupt()
Quindi, nonostante il suo nome, interrupt non ha un effetto diretto su un thread di esecuzione.
In particolare, se tale thread non sta eseguendo un’operazione bloccante, la chiamata ad interrupt non ha nessun effetto immediato.
Tuttavia, la successiva chiamata bloccante troverà lo stato di interruzione a vero ed uscirà immediatamente lanciando l’apposita eccezione.
È possibile conoscere lo stato di interruzione di un thread chiamando
public boolean isInterrupted()
Tale metodo restituisce l’attuale stato di interruzione di questo thread, senza modificarlo.
1) Implementare la classe Interruptor, il cui compito è quello di interrompere un dato thread dopo un numero fissato di secondi.
Ad esempio, se t è un riferimento ad un oggetto Thread, la linea
Interruptor i = new Interruptor(t, 10);
crea un nuovo thread di esecuzione che interrompe il thread t dopo 10 secondi.
2) Implementare un metodo statico delayIterator che prende come argomenti un iteratore “i” ed un numero intero “n”, e restituisce un nuovo iteratore dello stesso tipo di “i”, che restituisce gli stessi elementi di “i”, ma in cui ogni elemento viene restituito (dal metodo next) dopo un ritardo di “n” secondi.
Viene valutato positivamente l’uso di classi anonime.
Una applicazione dovrebbe sempre essere in grado di terminare tutti i suoi thread su richiesta.
Infatti, in un ambiente interattivo l’utente potrebbe richiedere la chiusura dell’applicazione in qualunque momento.
Per ottenere questo risultato, tutti i thread dovrebbero rispettare la seguente disciplina relativamente alle interruzioni.
Se una chiamata bloccante lancia l’eccezione InterruptedException, il thread dovrebbe interpretarla come una richiesta di terminazione, e reagire assecondando la richiesta.
Se un thread non utilizza periodicamente chiamate bloccanti, dovrebbe invocare periodicamente isInterrupted e terminare se il risultato è vero.
Avvertenze:
while (!Thread.currentThread().isInterrupted())
Riassumiamo qui i metodi della classe Thread esaminati, tutti pubblici
Thread()
costruttore senza argomenti
void start()
crea e avvia il corrispondente thread di esecuzione
void run()
l’entry point del thread
static Thread currentThread()
restituisce l’oggetto thread del thread di esecuzione corrente
static void sleep(long m) throws I.E.
attende m millisecondi
void join() throws I.E.
attende la terminazione di questo thread
void interrupt()
imposta lo stato di interruzione di questo thread
boolean isInterrupted()
restituisce lo stato di interruzione di questo thread
4. Risoluzione dell'overloading e dell'overriding
5. Controllo di uguaglianza tra oggetti
6. Classi interne, locali ed anonime
7. Iteratori, teoria e pratica
8. Clonazione di oggetti. Confronto tra oggetti.
9. Elementi di programmazione di interfacce grafiche
10. Il paradigma Model-View-Controller. Il pattern Strategy
11. I pattern Composite e Decorator
12. I pattern Template Method e Factory Method
13. Classi e metodi parametrici
14. La libreria Java Collection Framework: le interfacce Iterable, ...
15. La libreria Java Collection Framework: la classe HashSet e le l...
16. Parametri di tipo con limiti
17. L'implementazione della programmazione generica: la cancellazio...
18. La riflessione
19. Introduzione al multi-threading
22. Classi enumerate