I Thread e concorrenza
Lezione Concurrency (The Java Tutorials > Essential Classes) per vedere le due modalità di creazione dei thread.
Nel modello classico di programmazione, c'è un'unica central processor unit che legge le istruzioni da memoria e le esegue una dopo l'altra. Lo scopo di un programma è di provvedere una sequenza di istruzioni al processore per essere eseguito. Questo è l'unica tipo di programmazione che abbiamo considerato finora.
Tuttavia, questo modello di programmazione ha delle limitazioni. I moderni computer hanno molti processori, che permettono loro di compiere più task (lavori) contemporaneamente. Per utilizzare tutte la potenzialità di tutti questi processori, abbiamo bisogno di scrivere programmi che possono fare esecuzioni parallele (parallel processing). Per i programmatori java, questo significa imparare i thread. Un singolo thread è simile a un programma che finora abbiamo scritto, ma più di un thread possono essere eseguiti al medesimo momento, in "parallelo". Questo rende le cose più interessanti ma anche più difficili che la programmazione single thread (quella fatta finora) è che i thread in un programma sono raramente completamente indipendenti uno dall'altro. Essi in genere devono coordinarsi e comunicare tra loro.
Creazione ed esecuzione dei thread
In Java, è rappresentato da un oggetto appartenente alla classe java.lang.Thread
(o una sottoclasse di questa classe). Lo scopo di un oggetto di tipo Thread è di eseguire un singolo metodo e una sola volta. Questo metodo rappresenta il compito che deve essere eseguito dal thread. Il metodo è eseguito dal proprio flusso di esecuzione (thread of control), che può essere eseguito in parallelo con gli altri thread. Quando l'esecuzione del metodo del thread è terminato o perché il metodo è finito normalmente o a causa di un'eccezione non catturata, il thread finisce di essere attivo. Quando questo succede, non c'è modo per restartare il thread o di utilizzare lo stasso oggetto di tipo Thread
per far partire un altro thread.
Ci sono due modi per programmare un thread. Uno è di creare una sottoclasse di Thread e definire il metodo public void run()
nella sottoclasse. Questo metodo run()
definisce il compito che verrà eseguito dal thread. Cioè, quando il thread è fatto partire, è il metodo run()
che verrà eseguito nel thread. Per esempio, qui c'è una semplice, ma piuttosto inutile, classe che definisce un thread che non fa altro che stampare un messaggio in standard output:
Per utilizzare NamedThread
, bisogna chiaramente creare un oggetto appartenente a questa classe. Per esempio,
NamedThread greetings = new NamedThread("Fred");
Tuttavia, la semplice creazione dell'oggetto non manda in automatico l'esecuzione del thread o causa l'esecuzione del metodo run()
. Per fare questo, è necessario invocare il metodo start() dell'oggetto di tipo thread.
In questo esempio, viene fatto con lo statement:
greetings.start();
Lo scopo del metodo start()
è creare un nuovo flusso di esecuzione che eseguirà il metodo run()
dell'oggetto di tipo Thread
. Questo nuovo thread verrà eseguito in parallelo con il thread nel quale il metodo start()
è stato chiamato, insieme con gli altri thread che già esistevano. Il metodo start()
ritorna immediatamente avendo attivato il nuovo thread, senza aspettare che il thread abbia terminato. Questo significa che il codice nel metodo run() del thread è eseguito in parallelo del codice che segue la chiamata al metodo start(). Consideriamo il seguente codice:
Dopo che greetings.start()
è eseguito, ci sono due thread. Uno che scriverà "Thread has been started" mentre l'altro vuole scrivere "Greetings from thread 'Fred'!". E' importante notare che questi messaggi possono essere stampati in entrambi gli ordini. I due thread sono eseguiti simultaneamente e sono in competizione per l'accesso allo standard output, in modo da poter scrivere i propri messaggi. A quale dei due processi capita di essere il primo ad accedere, sarà il primo a scrivere il messaggio. In un normale programma single-thread, le cose accadono in modo definito, prevedibile dall'inizio alla fine. In un programma multi-thread, c'è fondamentalmente indeterminatezza. Noi non sappiamo con certezza in che ordine accadranno le cose. Questa indeterminatezza e quello che rende la programmazione parallela cosi difficile!
Da notare che chiamare greetings.start()
è molto diverso da chiamare greetings.run()
. Chiamare greetings.run()
eseguirà il metodo run()
nello stesso thread, piuttosto che creare un nuovo thread. Questo significa che tutto il lavoro del metodo run()
verrà fatto prima che il computer si muova sugli statement che seguono la chiamata a greetings.run()
. Non c'è parallelismo nè indeterminatezza.
Questa discussione assume che il computer su cui si stanno eseguendo i programmi abbia più di una CPU, in modo che si possibile che sia il thread originario che il nuovo thread creato siano effettivamente eseguiti in parallelo. Tuttavia, è possibile creare molti thread anche su computer che hanno solo un processore (e, più in generale, è possibile creare molti più thread di quanti siano i processori su un computer). In questo caso, i due thread competeranno nell'utilizzo dell'unica CPU. Tuttavia, c'è ancora indeterminatezza, in quanto il processore può cambiare dall'esecuzione di uno all'altro in modo imprevedibile. In effetti, dal punto di vista del programmatore, non c'è differenza se si programma su un computer mono-processore o multi-processore, e quindi fondamentalmente ignoreremo questa distinzione d'ora in avanti.
***********************
Abbiamo detto che ci sono due modi per programmare un thread. Il primo modo è definire una sottoclasse di Thread
. Il secondo è definire una classe che implementi l'interfaccia java.lang.Runnable
. L'interfaccia Runnable
definisce un singolo metodo, public void run()
. Dato un Runnable
, è possibile creare un Thread
il cui compito è eseguire il metodo run()
di Runnable
.
La classe Thread
ha un costruttore che prende un Runnable
come parametro. Quando un oggetto che implementa l'interfaccia Runnable
è passato al costruttore, il metodo run()
del thread semplicemente invocherà il metodo run()
del Runnable
, e l'invocazione del metodo start()
del thread, creerà un nuovo flusso di esecuzione (thread) in cui il metodo run()
del Runnable
è eseguito.
Per esempio, in alternativa alla classe NamedThead
, protremmo definire la classe:
Per utilizzare questa versione della classe, possiamo creare un oggetto di tipo NamedRunnable
e utilizzare questo oggetto per creare un oggetto di tipo Thread
:
Il vantaggio di fare in questo modo è che ogni oggetto può implementare l'interfaccia Runnable
che deve contenere il metodo run()
, che verrà poi eseguito in un thread separato. Questo metodo run()
ha accesso a ogni cosa nella classe, incluse le variabili e metodi private
.
************
Per aiutare a comprendere come molti thread sono eseguiti in parallelo, consideriamo il semplice programma javanotes8.ThreadTest1. Questo programma crea diversi thread. Il compito è quello di contare i numeri primi che sono minori di 5000000. (Il particolare task che è eseguito non è importante per il nostro scopo. E' solo un programma demo. Sarebbe insensato avere un programma reale che abbia molti thread che eseguono lo stesso lavoro). I thread che compiono questo task sono definiti nella seguente static nested class:
Il main program chiede quanti thread mandare in esecuzione, e poi crea e fa partire il numero specificati di thread:
Quando mando in esecuzione il programma con un solo thread, per l'esecuzione ci impiega circa 2.517 secondi. Quando mando in esecuzione il programma utilizzando otto thread, l'output è:
Altri esempi creazione thread: Creazione di thread
Operazioni sui thread
La maggior parte delle API sui thread sono nella classe Thread
della standard library. Tuttavia, iniziamo con un metodo legato ai thread della classe Runtime
, una classe che permette ai programmi Java di avere informazioni rispetto all'ambiente in cui sono in esecuzione. Quando facciamo programmazione parallela allo scopo di suddividere il lavoro tra più processori, potrebbe essere utile conoscere quanti processori ha il computer su cui gira l'applicazione. In Java si può conoscere il numero dei processori chiamando la funzione:
Runtime.getRuntime().availableProcessors()
che ritorna un int
con il numero dei processori che sono disponibili alla Java Virtual Machine.
**************
Un oggetto di tipo Thread
ha numerosi metodi per lavorare con i thread. Il più importante è il metodo start()
, che è stato visto prima.
Una volta che il thread è stato fatto partire (con il metodo start()
), continuerà finché il metodo run()
non terminerà per qualche motivo. Alcune volte è utile per un thread saper se un altro thread è terminato. Se thrd
è un oggetto di tipo Thread
, allora la funzione thrd.isAlive()
ritorna un boolean che può essere utilizzato per testare se thrd
è terminato o ancora "vivo" ("alive"). Un thread è vivo tra il momento in cui è fatto partire (tramite l'invocazione del metodo start()
) e il momento che termina. Dopo che un thread è terminato si dice che è "morto" ("dead"). Ricordiamoci che un thread una volta che è terminato non può essere ristartato (tramite l'invocazione di start()
).
Il metodo statico Thread.sleep(milliseconds)
causa al thread che esegue questo metodo di andare in "sleep" ("addormentato") per il numero specificato di millisecondi. Un thread in "sleep" è ancora vivo ma non "running" (non in esecuzione). Mentre un thread è in sleeping, il computer può eseguire ogni altro thread in esecuzione (o programma in esecuzione). Thread.sleep(milliseconds)
può essere utilizzato per mettere una pausa nell'esecuzione di un thread. Il metodo sleep()
può lanciare un'eccezione di tipo InterruptedException
, che è un'eccezione di quelle checked che è obbligatorio che sia gestita. In pratica questo significa che il metodo sleep()
è invocato all'interno di try .... catch
per catturare la possibile InterruptedException
:
Un thread può mandare un interrupt a un altro thread per risvegliarlo quando è in sleep o in pausa per certe altre ragioni. Un Thread
, thrd
, può essere interrotto chiamando il metodo thrd.interrupt()
. In questo modo si può mandare un segnale da un thread a un altro. Un thread sa di essere stato interrotto quando cattura la InterruptedException
. Fuori da un blocco catch per l'eccezione, un thread può sapere se è stato interrotto chiamando il metodo statico Thread.interrupted()
. Il metodo dice se il thread corrente - il thread che esegue il metodo - è stato interrotto. Esso ha anche la inusuale proprietà di pulire il flag di interrupted status del thread così che si può controllare solo una volta per l'interruzione. Molto spesso, non dobbiamo fare nulla in risposta a una InterruptedException
(eccetto la catch).
A volte è necessario che un thread aspetti la fine di un altro thread. Questo è fatto con il metodo join()
della classe Thread
. Supponiamo che thrd
è un Thread
. Allora, se un thread chiama thrd.join()
, allora il thread chiamante va in "sleep" finché thrd
non termina. Se thrd
è già terminato quando il metodo thrd.join()
è chiamato, allora semplicemente non ha alcun effetto. Il metodo join()
può lanciare InterruptedException
, che deve essere gestita come solitamente. Come esempio, il codice seguente fa partire diversi thread, aspetta che tutti abbiano terminato, e poi stampa il tempo passato per l'esecuzione dei thread:
Un lettore attento noterà che questo codice assume che l'eccezione InterruptedException
non verrà lanciata.
Per essere assolutamente sicuri che il thread worker[i] abbia terminato in un ambiente dove InterruptedException
sono possibili, avremmo dovuto fare qualcosa del tipo:
Un'altra versione del metodo join()
prende in input un parametro intero che specifica il numero massimo di millisecondi da aspettare. Una chiamata a thrd.join(m)
aspetterà o fino a che il thread thrd
abbia terminato o fino a che m millisecondi sono passati. Questo metodo può essere utilizzato per permettere a un thread di svegliarsi periodicamente per compiere qualche attività mentre attende. In questo frammento di codice, ad esempio, viene fatto partire un thread, thrd
, e poi viene stampato un carattere '.' ogni due secondi aspettando finché thrd
non termina:
Utilizzo della sleep per mandare in pausa il thread
Il metodo statico sleep(long)
della classe Thread
permette di mandare in pausa il thread che ha invocato il metodo per un certo numero di millisecondi.
Esempio in trythreads.simple.SleepMessages
:
Il metodo sleep()
può lanciare l'eccezione java.lang.InterruptedException
, eccezione non di tipo java.lang.RuntimeException
, quindi da gestire o dichiarare altrimenti nella signature del metodo che invoca Thread.sleep
. Questa è un'eccezione che sleep()
lancia quando un altro thread interrompe il thread che ha invocato la sleep()
ed è in attesa che la sleep()
termini.
In questo caso essendoci un solo thread l'InterruptedException
, nessun altro thread può interrompere il thread che invoca la sleep()
e quindi non è necessario gestirla ma viene dichiarata la throws
nella signature del main
.
Vediamo come un thread può interrompere un altro thread.
Interrupts
Un interrupt è un segnale mandato a un thread per indicargli che dovrebbe finire quello che sta facendo e quindi terminare o fare qualcos'altro. E' compito del programmatore decidere cosa fare quando è stato ricevuto un interrupt, ma è moto comune decidere di far terminare il thread.
Un thread invia un interrupt invocando il metodo interrupt()
sull'oggetto di tipo Thread
del thread che si vuole interrompere.
Once the thread is interrupted its interrupt status is set to true and then based on whether the thread is currently blocked or not following activity takes place:
If this thread is blocked in an invocation of the
wait()
,wait(long)
, orwait(long, int)
methods of the Object class, or of thejoin()
,join(long)
,join(long, int)
,sleep(long)
, orsleep(long, int)
, methods of this class, then its interrupt status will be cleared (set to false again) and it will receive anjava.lang.InterruptedException
.If a thread that is not blocked is interrupted, then the thread’s interrupt status will be set.
Methods related to thread interruption in Java Thread class
Apart from the method interrupt()
already discussed above there are two more methods in java.lang.Thread
class related to thread interruption interrupted()
and isInterrupted()
.
void interrupt()– Interrupts this thread.
static boolean interrupted()– Checks whether the current thread has been interrupted. Also clears the interrupted status of the thread.
boolean isInterrupted()– Tests whether this thread has been interrupted. This method doesn’t change the interrupted status of the thread in any way.
Join
This method waits until the thread on which it is called terminates. There are three overloaded versions of join()
method in Java Thread
class.
public final void join() throws InterruptedException– Waits indefinitely for this thread to die.
public final void join(long millis) throws InterruptedException– Waits at most the time in milliseconds for this thread to die.
public final void join(long millis, int nanos) throws InterruptedException– Waits at most the time in milliseconds plus additional time in nanoseconds for this thread to die
isAlive
Method, isAlive()
, tests if this thread is alive. A thread is alive if it has been started and has not yet died (run()
method terminated). Method returns true if thread is alive otherwise it returns false.
isAlive() method syntax
Esempio utilizzo dei metodi join(), join(millis), sleep(millis) e isAlive()
Ricapitolando l'utilizzo dei metodi con l'esempio trythreads.simple.SimpleThreads
:
Esempi prima parte
Pit stop
Esercizi riepilogo prima parte:
creazione dei thread;
utilizzo del metodo
Thread.sleep(long timemillis)
;utilizzo metodo
join()
diThread
per sincronizzarsi sulla fine di un thread;utilizzo del metodo
interrupt()
della classeThread
;
Last updated