Sei sulla pagina 1di 19

Il meccanismo multitasking adottato dalla maggior parte dei sistemi operativi consente di avere

più di un programma in esecuzione contemporaneamente. Il sistema operativo distribuisce il


tempo di esecuzione della CPU tra i diversi programmi, offrendo solo l’impressione di svolgere
attività in parallelo.

Il multitasking può essere eseguire in due modi:

 multitasking preemptive (preventivo): i programmi vengono interrotti dal SO senza


consultarli,

 multitasking cooperativo: i programmi vengono interrotti dal SO solo quando sono


disposti a cedere il controllo.

I sistemi operativi meno recenti sono sistemi a multitasking cooperativo, mentre i più recenti sono
sistemi a multitasking preemptive.

I programmi multithread estendono il concetto del multitasking a un livello più basso: sembra che
i singoli programmi possano eseguire più attività contemporaneamente. Ogni attività prende di
solito il nome di thread. I programmi che sono in grado di eseguire più di un thread alla volta sono
detti programmi multithread. In sostanza, la differenza principale tra processi multipli e thread
multipli riguarda il fatto che, mentre ogni processo prevede un set completo di variabili proprie, i
thread condividono gli stessi dati.

I programmi GUI (Graphical User Interface) prevedono un thread separato per raccogliere gli
eventi dell’interfaccia grafica che provengono dal sistema operativo ospite.

Esempio thread

Conviene iniziare lo studio esaminando un programma che non utilizza thread multipli. Questo
programma anima una pallina che rimbalza spostandola continuamente.

Ball ball = new Ball();


panel.add(ball);
for (int i = 1; i <= STEPS; i++) {
ball.move(panel.getBounds());
panel.paint(panel.getGraphics());
Thread.sleep(DELAY);
}

Il metodo statico sleep della classe Thread definisce una pausa per un determinato numero di
millisecondi (parametro DELAY), cioè interrompe temporaneamente l’attività del thread corrente.
Inoltre può generare un’eccezione InterruptedException, che verrà discussa più avanti.
Utilizzare thread multipli
Mostreremo adesso come è facile realizzare lo stesso programma in modo che venga eseguito in
più thread, in particolare mandando in esecuzione il codice che sposta la pallina in un thread
separato. In effetti, sarà possibile lanciare più palline, ciascuna delle quali sarà mossa da un
proprio thread. Il thread principale è in grado di verificare se un utente fa click sul pulsante ‘close’
mentre le palline stanno rimbalzando e può così elaborare l’azione “di chiusura”.

Di seguito è riportata una semplice procedura che permette di eseguire un’attività in un thread
separato:

1. Si inserisce il codice dell’attività nel metodo run di una classe che implementa l’interfaccia
Runnable. Esempio:

class MyRunnable implements Runnable {


public void run() {
// codice dell’attività
}
}

2. Si costruire un oggetto della classe Runnable:

Runnable r = new MyRunnable();

3. Si costruisce un oggetto Thread da Runnable e si avvia il thread:

Thread t = new Thread(r);


t.start();
Interrompere i thread
Un thread termina quando il suo metodo run ritorna. Non è possibile forzare l’interruzione di un
thread, anche se si può utilizzare il metodo interrupt per richiedere l’interruzione di un thread.

Quando si chiama il metodo interrupt su un thread, viene impostato lo stato di interruzione del
thread. Si tratta di un flag booleano che è presente in ogni thread e ogni thread deve verificare
periodicamente se è stato interrotto.

 Per scoprire se è stato impostato lo stato di interruzione, si deve prima chiamare il metodo
statico Thread.currentThread() per acquisire il thread corrente e poi chiamare il metodo
isInterrupted come segue:

while (!Thread.currentThread().isInterrupted() && altro lavoro da eseguire) {


// esegui altro lavoro
}

Tuttavia se un thread è bloccato, la chiamata che blocca, per esempio sleep o wait, viene
terminata da un’eccezione InterruptedException poiché non si può verificare lo stato di
interruzione.

Il linguaggio non richiede che un thread interrotto debba essere terminato. Infatti il thread
interrotto può decidere come reagire all’interruzione stessa. Tuttavia è abbastanza comune che un
thread voglia semplicemente interpretare un’interruzione come una richiesta di conclusione.

 Il metodo run di un simile thread assume la seguente forma:

public void run() {


try {

while (!Thread.currentThread().isInterrupted() && altro) {
// esegue lavoro
}
catch (InterruptedException e) {
// thread interrotto durante pausa (sleep) o attesa (wait)
}
finally {
// cleanup: se il thread usa file/socket bisognerebbe chiuderli
}
// uscire dal metodo run fa concludere il thread
}
Il controllo isInterrupted non è necessario se si chiama il metodo sleep dopo ogni
iterazione dell’esecuzione poiché si andrebbe comunque a finire nella clausola catch.

 Il metodo interrupted è un altro metodo statico che verifica se il thread corrente è stato
interrotto. Inoltre pulisce lo stato di interruzione ripristinandolo a false.
Stati dei thread
I thread possono si possono trovare in uno degli stati indicati di seguito:

 Nuovo: quando si crea un thread con l’operatore new, il thread non è ancora in esecuzione
quindi si trova nello stato di nuovo.

 Runnable: dopo aver richiamato il metodo start, il thread si trova nello stato runnable.

o I sistemi di schedulazione preemptive assegnano a ogni thread runnable una


porzione di tempo per eseguire la propria attività.
o Quando si esaurisce il tempo assegnato, il SO sospende il thread e dà a un altro
thread la possibilità di lavorare.
o Quando si seleziona il nuovo thread, il SO tiene conto delle priorità del thread,
come verrà illustrato più avanti.
o In un computer con più processori, ogni processore può eseguire un thread ed è
possibile eseguire più thread in parallelo. Ovviamente, se il numero di thread è
superiore a quello dei processori, lo schedulatore deve comunque suddividere i
tempi dei processori.

 Bloccato: si trova in questo stato quando si verificano una delle seguenti azioni:

o Il thread viene sospeso con una chiamata del metodo sleep,


o Il thread si blocca per eseguire un’operazione di I/O e aspetta finché non viene
conclusa,
o Se un thread sta aspettando un lock (trattati successivamente) detenuto da un altro
thread, l’altro thread deve rilasciare il lock. E’ possibile anche impostare un tempo
di timeout, scaduto il quale il thread si blocca.
o Il thread attende che si verifichi una determinata condizione (trattate
successivamente). In questo caso l’altro thread deve segnalare che la condizione
potrebbe risultare modificata.
o Si verifica una chiamata del metodo suspend del thread. Questo metodo è
comunque disapprovato e non lo si dovrebbe utilizzare.

 Concluso: si conclude per una delle ragioni indicate di seguito:

o Morte naturale perché si esce dal metodo run in modo naturale,


o Improvvisamente perché un’eccezione non catturata conclude il metodo run.
Proprietà dei thread
Priorità dei thread

Ogni thread ha una priorità. L’ impostazione predefinita prevede che un thread erediti la priorità
del suo thread genitore, cioè il thread che lo ha avviato.

 E’ possibile aumentare o diminuire la priorità di qualsiasi thread con il metodo setPriority.


Si può impostare la priorità con un valore qualsiasi compreso tra 1 e 10:

o MIN_PRIORITY (definito come 1 nella classe Thread)

o NORM_PRIORITY (definito come 5)

o MAX_PRIORITY (definito come 10)

Tuttavia le priorità dei thread dipendono fortemente dal sistema. Ad esempio Windows XP
prevede 7 livelli di priorità mentre nella macchina virtuale SUN per Linux le priorità
vengono ignorate e tutti i thread hanno la stessa priorità.

NB: se si utilizzano le priorità, si deve tenere conto di un errore comune iniziale. Se ci sono diversi
thread che hanno alta priorità e si bloccano raramente, i thread che hanno bassa priorità non
vengono mai eseguiti.

Thread demoni

Un thread demone è semplicemente un thread che ha il solo scopo di servirne altri. Quando
rimangono solo thread demoni, si esce dalla macchina virtuale poiché non c’è motivo di
mantenere in esecuzione il programma essendo questi thread “di supporto”.

 Per trasformare un thread user (normale) in un thread demone:

t.setDaemon(true);

Questo metodo deve essere chiamato prima di avviare il thread.


Gruppi di thread

Alcuni programmi contengono un buon numero di thread e può essere utile classificarli in
categorie (gruppi) in base alle loro funzionalità. Si consideri per esempio un browser Internet. Se
molti thread tentano di scaricare immagini da un server e l’utente fa click su un pulsante ‘stop’ per
interrompere il caricamento della pagina corrente, è comodo avere un modo per interrompere
tutti questi thread simultaneamente.

 Un gruppo di thread si costruisce utilizzando il seguente costruttore:

ThreadGroup g = new ThreadGroup(stringGroupName);

 Si aggiungono i thread al gruppo di thread specificando il gruppo nel costruttore:

Thread t = new Thread(g, threadName);

 Per scoprire se tutti i thread di un determinate gruppo sono ancora nello stato runnable:

if (g.activeCount() == 0) { /* tutti i thread sono interrotti*/ }

 Per interrompere tutti i thread in un gruppo di thread:

g.interrupt(); // interrompe tutti i thread del gruppo g


Sincronizzazione
Nella maggior parte delle applicazioni multithread, due o più thread devono condividere l’accesso
agli stessi oggetti. Se due thread accedono ad uno stesso oggetto simultaneamente
modificandone lo stato, è facile immaginare che i due potrebbero ostacolarsi. Lo stato in cui verrà
a trovarsi l’oggetto dipenderà dall’ordine con il quale i thread hanno invocato i metodi. Una
condizione di questo tipo prende il nome di race condition.

Esempio race condition

Per evitare il danneggiamento dei dati condivisi da più thread, è necessario apprendere come
sincronizzare gli accessi. Vedremo prima cosa succede se non si imposta un’adeguata
sincronizzazione e successivamente come sincronizzare l’accesso ai dati. Analizzeremo quindi le
due versioni (unsynch e synch) di un programma che simula una banca che gestisce un certo
numero di conti di deposito e si generano transazioni casuali di spostamento di denaro tra questi
conti.

Nella prima versione, quella senza sincronizzazione (unshynch) si può scoprire che si verificano
errori quasi subito oppure dopo un pò di tempo: alcune somme di denaro spariscono o vengono
create spontaneamente.

Spiegazione della race condition

Questo problema si verifica quando due thread cercando di aggiornare contemporaneamente un


determinato conto. Si considerino due thread che eseguono contemporaneamente la seguente
istruzione:

accounts[to] += amount;

Il problema è legato al fatto che questo sono operazioni atomiche. L’istruzione può essere
elaborata come segue:

1. Si carica accounts[to] in un registro


2. Si somma amount
3. Si sposta di nuovo il risultato in accounts[to]

A questo punto si supponga che il primo thread esegua i passi 1 e 2 e poi si interrompa. Si
supponga ora che si attivi il secondo thread, che aggiorna la stessa voce nell’array accounts. A
questo, si riattiva il primo thread che porta a termine il passo 3. Quest’ultima azione distrugge le
modifiche dell’altro thread. In questo modo si ottiene un risultato che non è più corretto.

Il vero problema riguarda il fatto che il compito del metodo transfer può essere interrotto in un
qualsiasi momento. Se si potesse garantire che il metodo venga eseguito fino alla fine prima che il
thread possa perdere il controllo, lo stato dei conti in banca non risulterebbe mai danneggiato.
Lock e Condition: bloccare gli oggetti
A partire da JDK 5.0, ci sono due meccanismi che permettono di proteggere un blocco di codice
rispetto ad accessi concorrenti. Le precedenti versioni di Java utilizzavano la parola chiave
synchronized mentre JDK 5.0 ha introdotto la classe ReentrantLock. La parola chiave synchronized
mette a disposizione automaticamente un lock associato ad una condizione. Sarà più facile
comprendere la parola chiave synchronized dopo aver visto i lock e le condizioni singolarmente.

Oggetti lock

 Lo schema di base per proteggere un blocco di codice con ReentrantLock è il seguente:

myLock.lock() // myLock è un oggetto ReentrantLock


try {
sezione critica
}
finally {
myLock.unlock(); // garantisce che il blocco non è attivo anche se si
// genera un’eccezione
}

Questa condizione garantisce che solo un thread alla volta possa entrare nella sezione
critica. Non appena un thread acquisisce il lock sull’oggetto myLock, nessun altro thread
può superare l’istruzione lock fino a quando il primo thread non sblocca l’oggetto myLock.

 Si può utilizzare un blocco per proteggere il metodo transfer della classe Bank:

...
private Lock bankLock = new ReentrantLock(); // ReentrantLock implementa Lock
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
// uguale al blocco del metodo transfer
}
finally {
bankLock().unlock();
}
}

Si ipotizzi che un thread chiami transfer e venga sospeso prima di completare l’esecuzione. Si
consideri che anche un secondo thread chiami transfer. Il secondo thread non può acquisire il lock
e rimane bloccato sulla chiamata del metodo lock (invocato su bankLock).

Osservare che ogni oggetto Bank ha il proprio ReentrantLock: solo se due thread tentano di
accedere allo stesso oggetto Bank, il lock ha lo scopo di serializzare il processo.

Il lock si dice rientrante perché un thread può acquisire ripetutamente un lock che già possiede.
Oggetti condizione

Può succedere che un thread entri in una sezione critica solo per scoprire che non può proseguire
fino a quando non si verifica una determinata condizione. Si utilizza un oggetto condizione per
gestire i thread che hanno acquisito un lock ma non possono svolgere un’attività concreta. Per
motivi storici gli oggetti condizioni vengono anche chiamati variabili condizione.

Nel nostro programma, ad esempio, non si vuole trasferire denaro da un conto che non ha i fondi
sufficienti per coprire il trasferimento.

 Osserviamo che non è possibile utilizzare il seguente codice:

if (accounts[from] >= amount) {


// in questo punto il thread potrebbe essere disattivato
bank.trasnfer(from, to, amount);
}

E’ molto probabile che il controllo passi a un altro thread mentre si è tra l’uscita del test e
la chiamata del metodo transfer.

 Per essere sicuri che nessun altro thread possa modificare il saldo tra il test e l’inserimento
si può utilizzare un lock:

bankLock.lock();
try {
while (accounts[from] < amount) {
// pausa
}
// trasferisce i fondi
}
finally {
bankLock.unlock();
}

Se non c’è abbastanza denaro in un conto si aspetta fino a quando altri thread hanno
aggiunto fondi sufficienti. Tuttavia, questo thread ha appena acquisito l’accesso esclusivo a
bankLock, per cui nessun thread ha la possibilità di effettuare un deposito. Questa è la
situazione nella quale vengono utili gli oggetti condizione.
 Un oggetto lock può essere associato a uno a più oggetti condizione. Si ottiene un oggetto
Condition con il metodo d’istanza newCondition della classe Lock:

...
Private Condition sufficientFunds;
public Bank(. . .) {
...
sufficientFunds = bankLock.newCondition();
}

 Se il metodo transfer scopre che non sono disponibili fondi sufficienti, chiama

sufficientFunds.await();

A questo punto si blocca il thread corrente e si attiva il lock in modo che altri thread hanno
la possibilità di aumentare il saldo del conto.

Dopo che il metodo ha chiamato il metodo await, entra in await set relativo a questa condizione.
Il thread non viene sbloccato quando è disponibile il lock, ma rimane bloccato fino a quando un
altro thread non chiama il metodo signalAll sulla stessa condizione .

 Ad esempio, quando un altro thread trasferisce del denaro deve chiamare

sufficientFunds.signalAll();

Questa chiamata sblocca tutti i thread che stanno aspettando la condizione


corrispondente. A questo punto il thread deve verificare di nuovo la condizione. Non si è
sicuri che la condizione venga verificata: il metodo signalAll segnala semplicemente ai
thread in attesa che può essere verificata e che vale la pena controllare di nuovo la
condizione.

 In generale, una chiamata di await si deve trovare sempre all’interno di un ciclo simile a
quello indicato di seguito

while (!(ok per proseguire))


condizione.await();

NB: è molto importante che qualche altro thread chiami il metodo signalAll. Quando un thread
chiama await non ha più modo di sbloccare se stesso e si affida agli altri thread. Se nessuno di
questi si occupa di sbloccare il thread in attesa, questo non viene più eseguito e ciò può portare a
spiacevoli deadlock.

Quando si chiama signalAll? La regola generale afferma che: si deve chiamare ogni volta che lo
stato di un oggetto cambia in un modo che può risultare vantaggioso per i thread in attesa, nel
nostro caso ogni volta che cambia il saldo di un conto.
 Ritornando al blocco del metodo transfer quindi:

bankLock.lock();
try {
while (accounts[from] < amount)
sufficientFunds.await();
// trasferisce i fondi
finally {
bankLock.unlock();
}
}

Per completare diciamo che un thread può chiamare await, signalAll oppure signal (sblocca un
solo thread a caso dall’await set) solo su un oggetto condizione di cui possiede il lock.

Riepilogo lock e condizioni

 Un lock protegge determinate sezioni di codice (sezioni critiche) e permette a un solo


thread per volta di eseguire il codice;

 Un lock gestisce i thread che tentano di entrare in un sezione di codice protetta;

 Un lock può essere associato a uno o piò oggetti condizione;

 Ogni oggetto condizione gestisce i thread che sono entrati in una sezione di codice protetta
ma non sono in grado di proseguire.

Prima che venissero aggiunte le interfacce Lock e Condition in JDK 5.0, il linguaggio Java utilizzava
un meccanismo di concorrenza diverso (synchronized) che verrà illustrato nel prossimo paragrafo.
La parola chiave synchronized

Ogni oggetto in Java ha un lock implicito. Se un metodo è dichiarato con la parola chiave
synchronized, il lock dell’oggetto protegge l’intero metodo.

 In altre parole, per chiamare il metodo un thread deve acquisire il lock dell’oggetto. Quindi:

public synchronized void method() {


// corpo del metodo
}

è equivale a:

public void method() {


bloccoImplicito.lock();
try {
// corpo del metodo
}
finally { bloccoImplicito.unlock(); }
}

Per esempio, invece di utilizzare un lock esplicito (bankLock) si può dichiarare semplicemente il
metodo transfer della classe SynchBank come synchronized.

 Il lock dell’oggetto implicito è associato a una sola condizione:

o Il metodo wait aggiunge un thread al set di attesa ed è equivalente a

condizioneImplicita.await();

o I metodi notifyAll/notify sbloccano i thread in attesa ed equivalgono a

condizioneImplicita.signalAll();
Esempio metodo transfer synchronized

 Possiamo implementare il metodo transfer della classe SynchBank come segue:

public synchronized void transfer(int from, int to, int amount) {


while (accounts[from] < amount)
wait();
accounts[from] -= amount;
accounts[to] += amount;
notifyAll(); // notifica tutti i thread in attesa della condizione
}
public synchronized double getTotalBalance() { . . . }

Si può osservare che l’utilizzo della parola chiave synchronized produce codice che risulta molto
più conciso. Ovviamente, per comprendere questo codice si deve sapere che ogni oggetto ha un
lock implicito e che il lock ha un condizione implicita:

 Il lock gestisce i thread che tentano di entrare in un metodo synchronized,

 La condizione gestisce i thread che hanno chiamato wait.

Tuttavia, i blocchi e le condizioni implicite hanno certi limiti, tra cui i seguenti:

 Non è possibile interrompere un thread che sta tentando di acquisire un lock;

 Non è possibile specificare un timeout quando si tenta di acquisire un lock;

 Può essere inefficiente avere una sola condizione per lock;

 Le primitive di lock della macchina virtuale non si accoppiano fedelmente con i meccanismi
di lock disponibili nell’hardware.
Cosa si deve utilizzare nel proprio codice, oggetti Lock e Condition oppure metodi synchronized?

1. Non conviene utilizzare né Lock/Condition né la parola chiave synchronized. In molti casi si


può utilizzare uno dei meccanismi del pacchetto java.util.concurrent che gestiscono tutte
le operazioni di lock. Per esempio, si può utilizzare una coda bloccante per sincronizzare i
thread che eseguono un’attività comune.

2. Se la parola chiave synchronized può andare bene in una situazione, conviene utilizzarla. Si
scrive meno codice e ci sono meno possibilità di errore.

3. Si utilizza Lock/Condition se è assolutamente necessario avere a disposizione le


potenzialità offerte da questo genere di costrutto.

E’ ammesso dichiarare metodi statici come sincronizzati. Se si chiama un metodo di questo tipo, si
acquisisce il lock dell’oggetto class associato. Per esempio, se la classe Bank ha un metodo statico
sincronizzato, quando lo si chiama si ottiene anche il lock dell’oggetto Bank.class.

Campi volatili

A volte può sembrare eccessivo pagare il prezzo della sincronizzazione solo per leggere o scrivere
uno o due campi istanza. La parola chiave volatile propone un meccanismo indipendente dai lock
che permette di accedere in modo sincronizzato a un campo istanza.

 Si consideri per esempio un oggetto che abbia un flag done di tipo boolean impostato da
un thread ed esaminato da un altro thread. Ci sono due possibilità:

1. Si utilizza un lock implicito, per esempio:

private boolean done;


public synchronized boolean isDone() { return done; }

Questa procedura ha un potenziale svantaggio: il metodo isDone può bloccare se un altro


thread ha bloccato l’oggetto.

2. Si dichiara il campo volatile:

private volatile boolean done;


public boolean isDone() { return done; }
Per riepilogare l’accesso concorrente a un campo è sicuro se si rispettano una delle tre condizioni:

1. Il campo è final e vi si può accedere dopo che il costruttore è stato eseguito.

2. L’accesso al campo è protetto da un lock.

3. Il campo è volatile

Verificare i lock e i timeout


Un thread si blocca indefinitivamente quando chiama il metodo lock per ottenere un lock
posseduto da un altro thread.

 Il metodo tryLock tenta di acquisire un lock e restituisce true se ha successo, altrimenti


restituisce false e il thread può andare oltre ed eseguire qualcos’altro:

if (myLock.tryLock()) {
//ora il thread possiede il lock
try { . . . }
finally {
myLock.unlock();
}
}
else
// esegue altre istruzioni

 Si può chiamare tryLock con un parametro di timeout, come indicato di seguito:

if (myLock.tryLock(100, TimeUnit.MILLISECONDS)) . . .

TimeUnit è un’enumerazione di valori SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS

 E’ possibile impostare un timeout anche quando si è in attesa di una condizione:

myCondition.await(100, TimeUnit.MILLISECONDS)) . . .

Il metodo await ritorna se un altro thread ha attivato questo thread chiamando signalAll
oppure signal, oppure se è trascorso il timeout o se è stato interrotto il thread.
Thread e Swing
Uno dei motivi per cui si utilizzano i thread nei programmi è legato al fatto che questi diventano
reattivi. Quando un programma deve svolgere del lavoro che richiede tempo, si può attivare un
altro thread così da non bloccare l’interfaccia utente.

Tuttavia, si deve fare attenzione a ciò che fa un thread, perché Swing non è thread-safe. Se si
cerca di elaborare gli elementi dell’interfaccia utente con thread multipli, si rischia di danneggiare
l’interfaccia utente. I progettisti Swing hanno deciso di non compiere lo sforzo di rendere Swing
thread-safe per un motivo in particolare:

- La sincronizzazione richiede tempo e non si vuole rallentare ulteriormente il


funzionamento di Swing.

Thread di distribuzione degli eventi: EventDispatcher

Ogni applicazione Java inizia con un metodo main che viene eseguito nel thread principale. In un
programma Swing il metodo main esegue di solito le operazioni indicate di seguito:

1. Prima chiama il costruttore che dispone i componenti in una finestra di frame.

2. Poi chiama il metodo setVisible sulla finestra di frame.

Quando viene mostrata la prima finestra, si crea un secondo thread, il thread di distribuzione
degli eventi (EventDispatcher) che esegue tutte le notifiche di eventi, per esempio le chiamate di
actionPerformed o paintComponent. Questo thread termina la sua esecuzione quando si esce dal
metodo main.

Regole da osservare

1. Se un’azione impiega molto tempo, si attiva un nuovo thread per svolgere l’attività.

2. Se un’azione può bloccare un input o un output, si attiva un nuovo thread: non si vuole
congelare l’interfaccia utente per un tempo potenzialmente infinito in attesa, ad esempio,
di una connessione in rete che non risponde.

3. I thread generati non devono modificare l’interfaccia utente. Conviene avviarli, completarli
e poi aggiornare l’interfaccia utente dal thread di distribuzione degli eventi.
Coda di eventi: EventQueue

Si consideri a questo punto di attivare un thread separato per svolgere un’attività che richiede
tempo. Si può, ad esempio, voler aggiornare l’interfaccia utente per indicare lo stato di
avanzamento dei lavori svolti nel thread. Ma il thread corrente non può toccare i componenti
dell’interfaccia grafica.

 Non si può chiamare, ad esempio, label.setText dal thread.

Per risolvere questo problema si possono utilizzare due metodi di supporto che consentono di
aggiungere azioni arbitrarie nella coda di eventi (EventQueue).

 Si utilizzano i metodi invokeLater e invokeAndWait della classe EventQueue per fare


eseguire la chiamata nel thread di distribuzione eventi.

o invokeLater: dopo che l’evento viene immesso nella coda di eventi ritorna subito il
controllo al thread principale: il metodo run viene eseguito in modo asincrono.

o invokeAndWait: interrompe il flusso del thread chiamante liberandolo quando il


metodo run è stato effettivamente eseguito.

Nel caso in cui si voglia aggiornare un’etichetta che indichi l’avanzamento dei lavori conviene
utilizzare il metodo invokeLater: gli utenti preferiscono far avanzare il thread di lavoro piuttosto
che avere un indicatore di avanzamento dei lavori più accurato.

 Per esempio di seguito è indicato come si aggiorna il testo di un’etichetta:

EventQueue.invokeLater(new
Runnable() {
public void run() {
label.setText(percentage + “% complete”);
}
});
Un esempio: SwingWorkerTest

Il programma di esempio SwingWorkerTest prevede comandi per caricare un file di testo e per
cancellare il processo di caricamento del file. Conviene provare il programma con un file grande,
per esempio il testo completo di un libro. Le attività svolte sono le seguenti:

1. Il file viene caricato in un thread separato;

2. Mentre si legge il file, la voce ‘Open’ del menu è disattivata, mentre è attiva la voce
‘Cancel’;

3. Dopo aver letto ogni riga, viene aggiornato un contatore di riga nella barra di stato;

4. Quando si è completato il processo di lettura, si riattiva la voce ‘Open’, la voce ‘Cancel’ si


disattiva e la riga di stato è impostata con l’indicazione ‘Done’.

L’esempio mostra le tre attività UI tipiche di un thread di lavoro:

1. Inizializza l’interfaccia grafica prima di iniziare il lavoro (init),

2. Dopo ogni unità di lavoro, aggiorna l’interfaccia grafica per mostrare l’avanzamento dei
lavori (update),

3. Dopo avere completato il lavoro, effettua la modifica finale dell’interfaccia grafica (finish).
La classe SwingWorkerTask semplifica l’implementazione di queste attività.

Si estende la classe, si imposta l’override dei metodi init, update e finish e si implementa la logica
per gli aggiornamenti dell’interfaccia grafica.

La superclasse contiene i metodi comodi doInit e doUpdate e doFinish che mettono a disposizione
il codice che permette di eseguire questi metodi nel thread di distribuzione di evento.

Si consideri il seguente esempio:

private void doInit() {


EventQueue.invokeLater(new Runnable() {
public void run() { init(); }
});

A questo punto si mette a disposizione un metodo work che contiene il lavoro da svolgere
nell’attività. Nel metodo work è necessario chiamare doUpdate, non update, dopo ogni unità di
lavoro. Per esempio, l’attività di lettura del file prevede questo metodo work:

public void work() /* non viene mostrata la gestione dell’eccezione */ {


Scanner in = new Scanner(new FileInputStream(filePath));
textArea.setText(“”);
while (!Thread.currentThread().isInterrupted() && in.hasNextLine()) {
lineNumber++;
line = in.nextLine();
textArea.append(line); textArea.append(“\n”);
doUpdate();
}
}

Potrebbero piacerti anche