7. I thread di esecuzione
7.1. Introduzione
Quando si avvia un'applicazione, questa viene eseguita in un flusso di esecuzione chiamato thread. La classe che modella un thread è la classe java.lang.Thread, di cui riportiamo alcune proprietà e metodi:
restituisce il thread attualmente in esecuzione | |
imposta il nome del thread | |
nome del thread | |
indica se il thread è attivo (true) o meno (false) | |
avvia l'esecuzione di un thread | |
metodo eseguito automaticamente dopo l'esecuzione del precedente metodo start | |
interrompe l'esecuzione di un thread per n millisecondi | |
operazione bloccante: attende il completamento del thread per passare all'istruzione successiva |
I costruttori più comunemente utilizzati sono i seguenti:
crea un riferimento a un'attività asincrona. Quest'ultima è ancora inattiva. L'attività creata deve disporre del metodo run: nella maggior parte dei casi verrà utilizzata una classe derivata da Thread. | |
Lo stesso, ma è l'oggetto Runnable passato come parametro che implementa il metodo run. |
Vediamo una prima applicazione che evidenzia l’esistenza di un thread di esecuzione principale, quello in cui viene eseguita la funzione main di una classe:
// utilizzo di thread
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// inizializzazione del thread corrente
Thread main=Thread.currentThread();
// visualizzazione
System.out.println("Thread courant : " + main.getName());
// si cambia il nome
main.setName("myMainThread");
// verifica
System.out.println("Thread courant : " + main.getName());
// ciclo infinito
while(true){
// recupera l'ora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// visualizzazione
System.out.println(main.getName() + " : " +H);
// arresto temporaneo
Thread.sleep(1000);
}//while
}//main
}//classe
Risultati 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
- è possibile accedere alle caratteristiche di questo thread tramite Thread.currentThread()
- il ruolo del metodo sleep. In questo caso, il thread che esegue main entra regolarmente in stato di sospensione 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 in modo "simultaneo" in diversi thread di esecuzione. Quando si dice che i thread vengono eseguiti simultaneamente, spesso si commette un abuso di linguaggio. Se la macchina dispone di un solo processore, come spesso accade ancora oggi, i thread condividono tale processore: ne dispongono, a turno, per un breve istante (alcuni millisecondi). È questo che crea l’illusione del parallelismo di esecuzione. Il tempo assegnato a un thread dipende da vari fattori, tra cui la sua priorità, che ha un valore predefinito ma può essere impostata anche tramite programmazione. Quando un thread ha a disposizione il processore, lo utilizza normalmente per tutto il tempo che gli è stato assegnato. Tuttavia, può liberarlo prima del termine:
- mettendosi in attesa di un evento (wait, join)
- mettendosi in sospensione per un periodo di tempo determinato (sleep)
- Un thread T può essere creato in vari modi
- derivando dalla classe Thread e ridefinendone il 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 deriva dalla classe Thread:
// si crea il thread i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//definizione dei compiti[i]
Il metodo run si limita qui a rinviare a un metodo affiche.
- L'esecuzione del thread T viene avviata da T.start(): questo metodo appartiene alla classe Thread ed esegue una serie di inizializzazioni, quindi avvia automaticamente il metodo run del thread o dell'interfaccia Runnable. Il programma che esegue l'istruzione T.start() non attende il completamento del thread T: passa immediatamente all'istruzione successiva. Si hanno quindi due thread che si eseguono in parallelo. Spesso devono potersi comunicare tra loro per sapere a che punto è il lavoro comune da svolgere. Questo è il problema della sincronizzazione dei thread.
- Una volta avviato, il thread thread viene eseguito in modo autonomo. Si interromperà quando la funzione run che sta eseguendo avrà terminato il proprio lavoro.
- È possibile attendere il completamento dell’esecuzione del thread T tramite T.join(). Si tratta di un’istruzione bloccante: il programma che la esegue rimane bloccato finché l’attività T non ha terminato il proprio lavoro. Anche questo è un metodo di sincronizzazione.
Esaminiamo il seguente programma:
// utilizzo dei thread
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// inizializzazione del thread corrente
Thread main=Thread.currentThread();
// si assegna un nome al thread corrente
main.setName("myMainThread");
// inizio della funzione main
System.out.println("début du thread " +main.getName());
// creazione di thread di esecuzione
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// creazione del thread i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//definizione dei compiti[i]
// si imposta il nome del thread
tâches[i].setName(""+i);
// si avvia l'esecuzione del thread i
tâches[i].start();
}//for
// fine di main
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// si recupera l'ora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// visualizzazione dell'inizio dell'esecuzione
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// sospensione per 1 s
try{
Thread.sleep(1000);
}catch (Exception ex){}
// si recupera l'ora
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// visualizzazione della fine dell'esecuzione
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// visualizza
}//classe
Il thread principale, quello che esegue la funzione main, crea altri 5 thread incaricati di eseguire il metodo statico affiche. 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 istruttivi:
- si nota innanzitutto 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 la propria esecuzione prima di loro. L'operazione
avvia l'esecuzione del thread tâches[i], ma una volta fatto ciò, l'esecuzione prosegue immediatamente con l'istruzione successiva senza attendere la fine dell'esecuzione del thread.
- Tutti i thread creati devono eseguire il metodo affiche. L’ordine di esecuzione è imprevedibile. Anche se nell’esempio l’ordine di esecuzione sembra seguire l’ordine di avvio dei thread, non è possibile trarne conclusioni generali. Il sistema operativo dispone in questo caso di 6 thread e un processore. Distribuirà il processore a questi 6 thread secondo regole proprie.
- Nei risultati si osserva una conseguenza del metodo sleep. Nell’esempio, è il thread 0 a eseguire per primo il metodo affiche. Viene visualizzato il messaggio di inizio esecuzione, dopodiché il thread esegue il metodo sleep, che lo sospende per 1 secondo. A quel punto perde il processore, che diventa così disponibile per un altro thread. L’esempio mostra che sarà il thread 1 a ottenerlo. Il thread 1 seguirà lo stesso percorso, così come gli altri thread. Al termine del secondo di sospensione del thread 0, la sua esecuzione potrà riprendere. Il sistema gli assegna il processore e il thread 0 può completare l’esecuzione del metodo affiche.
Modifichiamo il nostro programma per terminare il metodo main con le istruzioni:
// fine della routine
System.out.println("fin du thread " +main.getName());
// arresto dell'applicazione
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:
interrompe tutti i thread dell'applicazione e non solo il thread main. Il metodo main potrebbe voler attendere il completamento dell'esecuzione dei thread che ha creato prima di terminare a sua volta. Ciò può essere fatto con il metodo join della classe Thread:
// in attesa di tutti i thread
for(int i=0;i<tâches.length;i++){
// si attende il thread i
tâches[i].join();
}//for
// fine della funzione
System.out.println("fin du thread " +main.getName());
// arresto dell'applicazione
System.exit(0);
Si ottengono quindi 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. Importanza dei thread
Ora che abbiamo evidenziato l'esistenza di un thread predefinito, quello che esegue il metodo Main, e che sappiamo come crearne altri, soffermiamoci sull'utilità dei thread per noi e sul motivo per cui li presentiamo qui. Esiste una tipologia di applicazioni che si presta particolarmente bene all’uso dei thread: le applicazioni client-server su Internet. In un’applicazione di questo tipo, un server situato su una macchina S1 risponde alle richieste dei client situati su macchine remote C1, C2, ..., Cn.

Ogni giorno utilizziamo applicazioni Internet che rientrano in questo schema: servizi Web, posta elettronica, consultazione di forum, trasferimento di file... Nello schema sopra riportato, il server S1 deve servire i client Ci contemporaneamente. Se prendiamo l’esempio di un server FTP (File Transfer Protocol) che fornisce file ai propri clienti, sappiamo che un trasferimento di file può talvolta richiedere diverse ore. È ovviamente fuori discussione che un singolo cliente possa monopolizzare il server per un periodo così lungo. Di norma, il server crea un numero di thread di esecuzione pari al numero di clienti. Ogni thread è quindi incaricato di gestire un cliente specifico. Poiché il processore viene condiviso ciclicamente tra tutti i thread attivi della macchina, il server dedica un po’ di tempo a ciascun cliente, garantendo così la simultaneità del servizio.

7.4. Un orologio grafico
Consideriamo la seguente applicazione che visualizza una finestra con un orologio e un pulsante per arrestare o riavviare l’orologio:
![]() | ![]() | ![]() |
Affinché l’orologio funzioni, è necessario che un processo si occupi di aggiornare l’ora ogni secondo. Allo stesso tempo, è necessario monitorare gli eventi che si verificano nella finestra: quando l’utente clicca sul pulsante “Interrompi”, l’orologio dovrà essere fermato. Si tratta di due attività parallele e asincrone: l’utente può cliccare 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 viene eseguita la finestra possa quindi gestire l’orologio. Tuttavia, quando viene eseguito un metodo dell’applicazione grafica, il thread di quest’ultima non è più in ascolto degli eventi dell’interfaccia grafica. 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 il clic sul pulsante «Stop» può interromperlo. Tuttavia, questo evento verrà elaborato solo al termine del metodo. Si finisce per girare a vuoto.
La soluzione a questo problema sarebbe che, quando l’utente fa clic sul pulsante «Avvia», venga avviata un’attività per gestire l’orologio, ma che l’applicazione possa continuare a monitorare gli eventi che si verificano nella finestra. Si avrebbero quindi due attività distinte che verrebbero eseguite in parallelo:
- gestione del timer
- ascolto degli eventi della finestra
Torniamo al nostro orologio grafico:
![]() |
|
Il codice utile 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();
// attributi dell'istanza
boolean finHorloge=true;
//Costruire il frame
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// si esegue il ciclo finché non ci viene detto di fermarci
while( ! finHorloge){
// si recupera l'ora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// la si visualizza nel campo T
txtHorloge.setText(H);
// attesa di un secondo
try{
Thread.sleep(1000);
} catch (Exception e){
// uscita con errore
System.exit(1);
}//try
}// while
}// runHorloge
//Inizializza il componente
private void jbInit() throws Exception {
...................
}
//Sostituito, così possiamo uscire quando la finestra viene chiusa
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// si avvia/arresta il timer
// si recupera la didascalia del pulsante
String libellé=btnGoStop.getText();
// avviare?
if(libellé.equals("Lancer")){
// si crea il thread in cui verrà eseguito il timer
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//definire thread
// si autorizza l'avvio del thread
finHorloge=false;
// si modifica il testo del pulsante
btnGoStop.setText("Arrêter");
// si avvia il thread
thHorloge.start();
// fine
return;
}//if
// arresta
if(libellé.equals("Arrêter")){
// si ordina al thread di arrestarsi
finHorloge=true;
// si modifica il testo del pulsante
btnGoStop.setText("Lancer");
// fine
return;
}//if
}
}
Quando l'utente fa clic sul pulsante "Avvia", viene creato un thread utilizzando una classe anonima:
Il metodo run del thread rimanda al metodo runHorloge dell'applicazione. Una volta fatto ciò, il thread viene avviato:
Verrà quindi eseguito il metodo runHorloge:
private void runHorloge(){
// si ripete il ciclo finché non ci viene detto di fermarci
while( ! finHorloge){
// si recupera l'ora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// la si visualizza nel campo T
txtHorloge.setText(H);
// si attende un secondo
try{
Thread.sleep(1000);
} catch (Exception e){
// uscita con errore
System.exit(1);
}//prova
}// while
}// runHorloge
Il principio del metodo è il seguente:
- visualizza l'ora corrente nella casella di testo txtHorloge
- pausa di 1 secondo
- ripete il passaggio 1, dopo aver verificato la variabile booleana finHorloge, che verrà impostata su vero quando l'utente cliccherà sul pulsante Arrêter.
7.5. Applet orologio
Trasformiamo l’applicazione grafica precedente in un’applet con il metodo consueto 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 direttamente questo documento in IE facendo doppio clic su di esso, otteniamo la seguente visualizzazione:

In questo esempio, tutti gli elementi necessari per l’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 al caricamento dell'applet viene eseguito il metodo init e successivamente il metodo start, se presente. Inoltre, quando l’utente esce dalla pagina, viene eseguito il metodo stop, se presente. Quando torna alla pagina dell’applet, viene richiamato nuovamente il metodo start. Quando un’applet utilizza thread di animazione visiva, spesso si ricorre ai metodi start e stop dell’applet per avviare e arrestare i thread. È infatti inutile che un thread di animazione visiva continui a lavorare in background mentre l’animazione è nascosta.
Aggiungiamo quindi alla nostra applet i seguenti metodi start e stop:
public void stop(){
// la pagina è nascosta
// monitoraggio
System.out.println("Page stop");
// la pagina è nascosta - si interrompe il thread
finHorloge=true;
}
public void start(){
// la pagina riappare
// monitoraggio
System.out.println("Page start");
// si avvia un nuovo thread di timer se necessario
if(btnGoStop.getText().equals("Arrêter")){
// si modifica la dicitura
btnGoStop.setText("Lancer");
// e si simula il clic dell'utente
btnGoStop_actionPerformed(null);
}//if
}//start
Inoltre, abbiamo aggiunto un monitoraggio nel metodo run del thread per sapere quando si avvia e quando si arresta:
private void runHorloge(){
// monitoraggio
System.out.println("Thread horloge lancé");
// si ripete il ciclo finché non ci viene detto di fermarci
while( ! finHorloge){
// recuperiamo l'ora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// la si visualizza nel campo T
txtHorloge.setText(H);
// attesa di un secondo
try{
Thread.sleep(1000);
} catch (Exception e){
// uscita con errore
return;
}//try
}// while
// monitoraggio
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 avviata - pagina visualizzata
Thread horloge lancé // il thread viene avviato di conseguenza
Page stop // applet ridotta a icona
Thread horloge terminé // il thread viene arrestato di conseguenza
Page start // applet visualizzata nuovamente
Thread horloge lancé // il thread viene riavviato
Thread horloge terminé // pressione del pulsante "Interrompi"
Thread horloge lancé // pressione del pulsante "Avvia"
Page stop // applet ridotta a icona e
Thread horloge terminé // thread arrestato di conseguenza
Page start // ri-visualizzazione dell'applet
Thread horloge lancé // thread riavviato di conseguenza
Con AppletViewer, l'evento start si verifica quando la finestra di AppletViewer è visibile, mentre l'evento stop si verifica quando la finestra 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 garantito dal task principale, che impostava un valore booleano per arrestare il thread dell’orologio. Affrontiamo ora il problema dell’accesso concorrente dei task alle risorse comuni, problema noto anche come “condivisione delle risorse”. Per illustrarlo, esamineremo innanzitutto un esempio.
7.6.1. Un conteggio non sincronizzato
Consideriamo la seguente interfaccia grafica:

n. | tipo | nome | ruolo |
1 | JTextField | txtAGénérer | indica il numero di thread da generare |
2 | JTextfield (non modificabile) | txtGénéres | indica il numero di thread generati |
3 | JTextField (non modificabile) | txtStatus | fornisce informazioni sugli errori riscontrati e sull'applicazione stessa |
4 | JButton | btnGénérer | avvia la generazione dei thread |
Il funzionamento dell'applicazione è il seguente:
- l'utente indica il numero di thread da generare nel campo 1
- avvia la generazione di questi thread tramite 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 questa sede intendiamo illustrare i problemi che si presentano in una situazione del genere. Ecco un esempio di esecuzione:

Si nota che è stata richiesta la generazione di 1000 thread, ma ne sono stati conteggiati solo 7. Il codice rilevante dell’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();
// variabili di istanza
Thread[] tâches=null; // i thread
int[] compteurs=null; // i contatori
//Costruire il frame
public interfaceSynchro() {
..........
}
//Inizializzare il componente
private void jbInit() throws Exception {
......................
}
//Sostituito, in modo da poter uscire quando la finestra viene chiusa
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//Generazione dei thread
// si legge il numero di thread da generare
int nbThreads=0;
try{
// Lettura del campo contenente il numero di thread
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//errore
txtStatus.setText("Nombre invalide");
// si ricomincia
txtAGénérer.requestFocus();
return;
}//catch
// all'inizio nessun thread generato
txtGénérés.setText("0"); // contatore delle attività a 0
// si generano e si avviano i thread
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// si crea il thread i
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//thread i
// si definisce il suo nome
tâches[i].setName(""+i);
// si avvia la sua esecuzione
tâches[i].start();
}//for
}//generare
// incrementare
private void incrémente(){
// si recupera il numero del thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// si legge il valore del contatore delle attività
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// lo si incrementa
compteurs[iThread]++;
// si attende 100 millisecondi - il thread perderà quindi il controllo del processore
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// si visualizza il nuovo contatore
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// monitoraggio
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// incrementa
}// classe
Analizziamo il codice:
- la finestra dichiara due variabili di istanza:
L'array tâches sarà l'array dei thread generati. L'array compteurs sarà associato all'array tâches. Ogni attività disporrà di un proprio contatore per recuperare il valore del campo txtGénérés dall'interfaccia grafica.
- Quando si fa clic sul pulsante Générer, viene eseguito il metodo btnGénérer_actionPerformed.
- Quest’ultimo inizia recuperando il numero di thread da generare. Se necessario, viene segnalato un errore se tale numero non è valido. Successivamente genera i thread richiesti, avendo cura di annotarne i riferimenti in un array e assegnando a ciascuno di essi un numero. Il metodo run dei thread generati rimanda al metodo incrémente della classe. Tutti i thread vengono avviati (start). Viene inoltre creato l'array dei contatori associati ai thread.
- il metodo incrémente:
- legge il valore attuale del campo txtGénérés e lo memorizza nel contatore appartenente al thread attualmente in esecuzione
- si ferma per 100 ms, in modo da liberare intenzionalmente il processore
- visualizza il nuovo valore nel campo txtGénérés
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 passa per primo e legge il valore 0 dal contatore. Lo porta quindi a 1 ma non lo scrive nella finestra: si interrompe volontariamente per 100 ms. Perde così il controllo del processore, che viene assegnato a un altro thread. Quest’ultimo opera allo stesso modo del precedente: legge il contatore della finestra e rileva lo 0 che vi si trova ancora. Imposta il contatore su 1 e, come il precedente, si interrompe per 100 ms. Il processore viene quindi nuovamente assegnato al primo thread: quest’ultimo scriverà il valore 1 nel contatore della finestra e terminerà l’esecuzione. Il processore viene ora assegnato al secondo thread, che a sua volta scriverà 1. Si ottiene un risultato errato.
Da dove deriva il problema? Il secondo thread ha letto un valore errato poiché il primo era stato interrotto prima di aver completato il proprio lavoro, che consisteva nell’aggiornare il contatore nella finestra. Questo ci porta al concetto di risorsa critica e di sezione critica di 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 della 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. È necessario garantire che, durante questa sezione critica, il thread sia l’unico ad avere accesso alla risorsa.
7.6.2. Un conteggio sincronizzato tramite metodo
Nell’esempio precedente, ogni thread eseguiva il metodo incrémente della finestra. Il metodo incrémente era dichiarato come segue:
Ora lo dichiariamo in modo diverso:
La parola chiave synchronized indica che solo un thread alla volta può eseguire il metodo *incrémente*. Consideriamo le seguenti notazioni:
- l’oggetto finestra F che crea i thread in btnGénérer_actionPerformed
- due thread T1 e T2 creati da F
Entrambi i thread vengono creati da F e poi avviati. Entrambi, quindi, eseguiranno il metodo F.run. Supponiamo che T1 arrivi per primo. Esegue F.run e poi F.incremente, che è un metodo sincronizzato. Legge il valore 0 del contatore, lo incrementa e poi si ferma per 100 ms. Il processore viene quindi assegnato a T2, che a sua volta esegue F.run e poi F.incremente. A questo punto rimane bloccato perché il thread T1 sta eseguendo F.incremente e la parola chiave synchronized garantisce che un solo thread alla volta possa eseguire F.incremente. T2 perde quindi a sua volta il controllo del processore senza essere riuscito a leggere il valore del contatore. Trascorsi i 100 ms, T1 riacquista il controllo del processore, visualizza il valore 1 del contatore, esce da F.incremente e poi da F.run e termina. T2 recupera quindi il processore e questa volta può eseguire F.incremente poiché T1 non sta più eseguendo questo metodo. T2 legge quindi il valore 1 del contatore, lo incrementa e si ferma per 100 ms. Trascorsi 100 ms, riprende il controllo del processore, visualizza il valore 2 del contatore e termina a sua volta. Questa volta, il valore ottenuto è corretto. Ecco un esempio testato:

7.6.3. Conteggio sincronizzato da un oggetto
Nell’esempio precedente, l’accesso al contatore txtGénérés è stato sincronizzato tramite un metodo. Se la finestra che crea i thread si chiama F, si può anche dire che il metodo F.incremente rappresenta una risorsa che doveva essere utilizzata da un solo thread alla volta. Si tratta quindi di una risorsa critica. L'accesso sincronizzato a questa risorsa è stato garantito dalla parola chiave synchronized:
Si potrebbe anche dire che la risorsa critica sia l'oggetto F stesso. Si tratta di un caso più restrittivo rispetto a quello in cui la risorsa critica è F.incremente. Infatti, in quest'ultimo caso, se un thread T1 esegue F.incremente, un thread T2 non potrà eseguire F.incremente, ma potrà eseguire un altro metodo dell’oggetto F, indipendentemente dal fatto che sia sincronizzato o meno. Nel caso in cui l’oggetto F sia esso stesso la risorsa critica, quando un thread T1 esegue una sezione sincronizzata di tale oggetto, qualsiasi altra sezione sincronizzata dell’oggetto diventa inaccessibile agli altri thread. Pertanto, se un thread T1 esegue il metodo sincronizzato F.incremente, un thread T2 non potrà eseguire non solo F.incremente, ma anche qualsiasi altra sezione sincronizzata di F, anche se nessun thread la sta utilizzando. Si tratta quindi di un metodo più restrittivo.
Supponiamo quindi che la finestra diventi la risorsa critica. Si scriverà quindi:
// incremento
private void incrémente(){
// sezione critica
synchronized(this){
// si recupera il numero del thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// si legge il valore del contatore delle attività
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// lo si incrementa
compteurs[iThread]++;
// si attende 100 millisecondi - il thread perderà quindi il controllo del processore
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// si visualizza il nuovo contatore
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//synchronized
}// incrementa
Tutti i thread utilizzano la finestra this per la sincronizzazione. Durante l'esecuzione, si ottengono gli stessi risultati corretti di prima. È infatti possibile sincronizzarsi su qualsiasi oggetto noto a tutti i thread. Ecco, ad esempio, un altro metodo che fornisce gli stessi risultati:
// variabili di istanza
Thread[] tâches=null; // i thread
int[] compteurs=null; // i contatori
Object synchro=new Object(); // un oggetto di sincronizzazione dei thread
// incrementa
private void incrémente(){
// sezione critica
synchronized(synchro){
..............
}//sincronizzato
}// incrementato
La finestra crea un oggetto di tipo Object che servirà per la sincronizzazione dei thread. Questo metodo è migliore di quello che si sincronizza sull'oggetto this perché meno vincolante. In questo caso, se un thread T1 si trova nella sezione sincronizzata di incrémente eun thread T2 voglia eseguire un'altra sezione sincronizzata dello stesso oggetto this, ma sincronizzata da un oggetto diverso da synchro, potrà farlo.
7.6.4. Sincronizzazione tramite eventi
In questo caso, utilizziamo un valore booleano peutPasser per indicare a un thread se può o meno entrare in una sezione critica. Un codice senza sincronizzazione potrebbe essere il seguente:
while(! peutPasser); // si attende che peutPasser diventi vero
peutPasser=false; // nessun altro thread deve passare
section critique; // qui il thread è da solo
peutPasser=true; // un altro thread può passare nella sezione critica
La prima istruzione, in cui un thread rimane in attesa che peutPasser diventi vero, è poco efficiente: il thread occupa inutilmente il processore. Si parla di attesa attiva. È possibile migliorare il codice come segue:
while(! peutPasser){ // si attende che peutPasser diventi vero
Thread.sleep(100); // arresto per 100 ms
}
peutPasser=false; // nessun altro thread deve passare
section critique; // qui il thread è da solo
peutPasser=true; // un altro thread può passare nella sezione critica
Il ciclo di attesa qui è migliore: se il thread non può passare, entra in stato di sospensione per 100 ms prima di verificare nuovamente se può passare o meno. Nel frattempo, il processore verrà assegnato ad altri thread del sistema.
Entrambi questi metodi sono in realtà errati: non impediscono a due thread di entrare contemporaneamente nella sezione critica. Supponiamo che un thread T1 rilevi che peutPasser è vero. A quel punto passerà all’istruzione successiva, dove reimposterà peutPasser a falso per bloccare gli altri thread. Tuttavia, potrebbe benissimo essere interrotto in quel momento, sia perché il suo tempo di processore è esaurito, sia perché un’attività con priorità maggiore ha richiesto il processore, o per un altro motivo. Il risultato è che perde il controllo del processore. Lo riottenerà poco dopo. Nel frattempo, altre attività otterranno il controllo del processore, tra cui forse un thread T2 che rimane in attesa in un ciclo infinito finché peutPasser non diventa vero. Anche lui scoprirà che peutPasser è a "vero" (il primo thread non ha avuto il tempo di impostarlo a "falso") ed entrerà a sua volta nella sezione critica. Cosa che non doveva accadere.
La sequenza
while(! peutPasser){ // si attende che peutPasser diventi vero
try{
Thread.sleep(100); // pausa di 100 ms
} catch (Exception e) {}
}// while
peutPasser=false; // nessun altro thread deve passare
è una sequenza critica che deve essere protetta tramite sincronizzazione. Ispirandosi all'esempio precedente, si può scrivere:
synchronized(synchro){
while(! peutPasser){ // si attende che peutPasser diventi vero
try{
Thread.sleep(100); // pausa di 100 ms
} catch (Exception e) {}
}//while
peutPasser=false; // nessun altro thread deve passare
}// sincronizzato
section critique; // qui il thread è da solo
peutPasser=true; // un altro thread può passare nella sezione critica
Questo esempio funziona correttamente. È possibile migliorarlo evitando l'attesa semi-attiva del thread quando controlla regolarmente il valore del booleano peutPasser. Invece di risvegliarsi regolarmente ogni 100 ms per verificare lo stato di peutPasser, può entrare in stato di sospensione e richiedere di essere risvegliato quando peutPasser sarà vero. Si scrive nel modo seguente:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se non è possibile passare, allora si attende
} catch (Exception e){
…
}
}
peutPasser=false; // nessun altro thread deve passare
}// synchronized
L'operazione synchro.wait() può essere eseguita solo da un thread "proprietario" momentaneo dell'oggetto synchro. In questo caso, la sequenza è:
synchronized(synchro){
…
}// synchronized
che garantisce che il thread sia proprietario dell’oggetto synchro. Tramite l’operazione synchro.wait(), il thread cede la proprietà del blocco di sincronizzazione. Perché? In genere perché gli mancano le risorse per continuare a lavorare. Quindi, piuttosto che bloccare gli altri thread in attesa della risorsa synchro, la cede e si mette in attesa della risorsa che gli manca. Nel nostro esempio, attende che il valore booleano peutPasser diventi vero. Come verrà avvisato di questo evento? Nel modo seguente:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se non è possibile passare, allora si attende
} catch (Exception e){
…
}
}
peutPasser=false; // nessun altro thread deve passare
}// synchronized
section critique...
synchronized(synchro){
synchro.notify();
}
Consideriamo il primo thread che supera il blocco di sincronizzazione. Chiamiamolo T1. Immaginiamo che trovi il valore booleano peutPasser vero, poiché è il primo. Lo imposta quindi su falso. Esce quindi dalla sezione critica bloccata dall'oggetto synchro. Un altro thread potrà quindi entrare nella sezione critica per verificare il valore di peutPasser. Lo troverà falso e si metterà quindi in attesa di un evento (wait). Così facendo, cede la proprietà dell’oggetto synchro. Un altro thread può quindi entrare nella sezione critica: anche lui si metterà in attesa poiché peutPasser è falso. Si possono quindi avere più thread in attesa di un evento sull’oggetto synchro.
Torniamo al thread T1, al quale è stato concesso il passaggio. Esso esegue la sezione critica, quindi segnala che un altro thread può ora passare. Lo fa con la sequenza:
synchronized(synchro){
synchro.notify();
}
Deve innanzitutto riprendere possesso dell’oggetto synchro con l’istruzione synchronized. Ciò non dovrebbe rappresentare un problema, poiché è in competizione con thread che, se ottengono momentaneamente l’oggetto synchro, devono rilasciarlo tramite un wait poiché rilevano che peutPasser è falso. Pertanto, il nostro thread T1 finirà per ottenere la proprietà dell’oggetto synchro. Fatto ciò, indica tramite l’operazione synchro.notify che uno dei thread bloccati da un synchro.wait deve essere riattivato. Successivamente rinuncia nuovamente alla proprietà dell’oggetto synchro, che verrà quindi assegnata a uno dei thread in attesa. Quest’ultimo prosegue l’esecuzione con l’istruzione che segue il wait che lo aveva messo in attesa. A sua volta, eseguirà la sezione critica ed eseguirà un synchro.notify per liberare un altro thread. E così via.
Vediamo come funziona questo meccanismo sull’esempio del conteggio già analizzato.
void btnGénérer_actionPerformed(ActionEvent e) {
//generazione dei thread
// si legge il numero di thread da generare
int nbThreads=0;
try{
// lettura del campo contenente il numero di thread
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//errore
txtStatus.setText("Nombre invalide");
// si ricomincia
txtAGénérer.requestFocus();
return;
}//catch
// RAZ contatore delle attività
txtGénérés.setText("0"); // contatore attività a 0
// il primo thread può passare
peutPasser=true;
// si generano e si avviano i thread
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// si crea il thread i
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//thread i
// si definisce il suo nome
tâches[i].setName(""+i);
// si avvia la sua esecuzione
tâches[i].start();
}//for
}//generare
Ora i thread non eseguono più il metodo incrémente, ma il seguente metodo synchronise:
// fase di sincronizzazione dei thread
public void synchronise(){
// si richiede l'accesso alla sezione critica
synchronized(synchro){
try{
// è possibile passare?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// si è passati - si impedisce agli altri thread di passare
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// sezione critica
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// abbiamo finito - liberiamo un eventuale thread bloccato all'ingresso della sezione critica
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// synchronized
} // sincronizza
Il metodo synchronise ha lo scopo di elaborare i thread uno alla volta. A tal fine utilizza una variabile di sincronizzazione denominata synchro. Il metodo incrémente non è più protetto dalla parola chiave synchronized:
// incrementa
private void incrémente(){
// si recupera il numero del thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// si legge il valore del contatore delle attività
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// lo si incrementa
compteurs[iThread]++;
// si attende 100 millisecondi - il thread perderà quindi il controllo del processore
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// si visualizza il nuovo contatore
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// incremento
Per 5 thread, i risultati ottenuti 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 5 thread generati dall'applicazione. T0 acquisisce per primo il blocco synchro e rileva che peutPasser è vero. Imposta peutPasser su falso e passa: questo è il significato del primo messaggio passé. Con ogni probabilità, prosegue ed esegue la sezione critica, in particolare il metodo incrémente. All’interno di questa, entra in stato di attesa per 100 ms (sleep). Rilascia quindi il processore. Questo viene assegnato a un altro thread, il thread T1, che ottiene così la proprietà dell’oggetto synchro. Scopre di non poter procedere e si mette in attesa (wait). A quel punto rilascia la proprietà dell’oggetto synchro e il processore. Quest’ultimo viene assegnato al thread T2, che subisce la stessa sorte. Durante i 100 ms di arresto di T0, i thread da T1 a T4 vengono quindi messi in attesa. Questo è il significato dei 4 messaggi «in attesa». Dopo 100 ms, T0 riacquista il controllo del processore e termina il proprio lavoro: questo è il significato del messaggio «0 terminé». Successivamente libera uno dei thread bloccati e termina. Il processore liberato viene quindi assegnato a un thread disponibile: quello che è stato appena liberato. In questo caso si tratta di T1. Il thread T1 entra quindi nella sezione critica: questo è il significato del messaggio «1 passé». Esegue ciò che deve fare e a sua volta si ferma per 100 ms. Il processore è quindi disponibile per un altro thread, ma tutti sono in attesa di un evento: nessuno di essi può appropriarsi del processore. Dopo 100 ms, il thread T1 riacquista il processore e termina: questo è il significato del messaggio "1 terminé". I thread da T1 a T4 avranno lo stesso comportamento di T1: questo è il significato delle tre serie di messaggi: «passato», «terminato».



