NOTA! Questo sito utilizza i cookie e tecnologie simili.

Se non si modificano le impostazioni del browser, l'utente accetta. Per saperne di piu'

Approvo

Capitolo 147: I Thread in Java.

 

La programmazione multithread permette di svolgere più operazioni ‘contemporaneamente'.

 

Le virgolette sono d'obbligo in quanto solo su sistemi multiprocessore, con sistemi operativi adatti ed in assenza di conflitti R—W (lettura o scrittura) sui dati, due o pi cose possono avvenire CONTEMPORANEAMENTE.

 

Java 3D I Thread in Java schema funzionamento

 

In pratica, su un singolo processore vengono eseguiti più compiti (‘task': da qui il termine ‘multitasking', ossia il poter lavorare con più cose e su oggetti diversi contemporaneamente), a ciascuno dei quali è dedicato un intervallo di tempo (‘slice', lett.: fetta) per volta.

 

 

CONSIGLI D'UTILIZZO

Quando utilizzare i thread ?

Ad esempio, quando stiamo copiando dei file (nel frattempo potremmo utilizzare un browser web, ecc) o, nel caso dei videogiochi, per gestire la GUI del gioco e controllare gli eventi che arrivano dalla rete (gioco multiplayer).

Come per ogni cosa, anche un uso eccessivo dei thread può portare a conseguenze spiacevoli.

A parte lo spreco di risorse e la riduzione della velocità di esecuzione, potrebbe essere necessario dover svolgere un'operazione o un blocco di codice COMPLETAMENTE, cioè senza interruzioni (con salti ad altri thread): questo avviene, ad esempio, quando si lavora con variabili condivise (es.: il giocatore X si sposta dalla casella 1 alla casella 2, mentre il giocatore Y spara un colpo verso la casella 1: l'aver colpito o meno X dipenderà quindi dallo scheduling dei thread di gestione del movimento e di verifica delle collisioni; può anche verificarsi, ad esempio, il caso in cui il thread che aggiorna l'aspetto grafico venga interrotto quando l'aggiornamento è a metà dello schermo per passare al thread che calcola le nuove posizioni dei giocatori: avremo quindi giocatori che nella parte superiore dello schermo avranno una parte del corpo in una posizione e, nella seconda metà dello schermo, la parte restante del corpo in un'altra posizione !!!).

Per risolvere alcuni problemi legati allo SCHEDULING e all'ESECUZIONE ATOMICA (atomica nel senso di ‘indivisibile', non frammentabile) dei thread viene introdotto il concetto di SINCRONIZZAZIONE.

 

 

LA SINCRONIZZAZIONE

La sincronizzazione permette ad un metodo di bloccare (lock) e sbloccare (unlock) l'accesso ad una variabile.

Va quindi utilizzata quando due o più thread devono accedere allo stesso campo o oggetto.

Il lock viene rilasciato quando l'esecuzione del metodo o comunque della porzione di codice da tenere d'occhio è terminata, sia che questo avvenga normalmente che con una eccezione.

L'utilizzo della sincronizzazione introduce ritardi e la possibilità che si verifichino deadlock, per cui bisognerebbe evitarlo quando non strettamente necessario (ad es.: non c'è bisogno di sincronizzare metodi che usano solo variabili locali, o blocchi di codice che non vengono utilizzati da più thread; è inoltre possibile sincronizzare solo una parte, quella ‘pericolosa', di un metodo, e non necessariamente TUTTO il metodo !!!).

 

 

DEADLOCK

Un deadlock (o ‘stallo') è una situazione che si verifica quando due o più thread restano l'uno in attesa dell'altro senza possibilità di sbloccare tale situazione (a meno di una terminazione esplicita dell'esecuzione, o di un'altra soluzione ‘deus ex machina').

Ad esempio, una situazione che produce un deadlock è la seguente: supponiamo che ci siano due thread che necessitino di due variabili, x e y, per eseguire le loro operazioni; allora, se si verifica che:

 

  • il thread A fa un lock sulla variabile x;

  • il thread B fa un lock sulla variabile y;

  • il thread A tenta di acquisire la variabile y, ma non ci riesce perchè è già acquisita da B e, nel mentre...

  • ... il thread B tenta di acquisire la variabile x, ma non ci riesce perchè è già acquisita da A, che è fermo in attesa

 

si ha un deadlock !

(NOTA: questo esempio è un pò troppo banale, anche perchè esistono primitive di lock che permettono di effettuare il lock su più variabili contemporaneamente al fine di evitare problemi del genere... comunque, è sufficiente ad illustrare come possano verificarsi problemi di questo tipo).

C'è un rischio di deadlock, quindi, quando più thread tentano di acquisire dei lock senza un ordine preciso. Per evitare i deadlock è necessario progettare l'esecuzione tenendo conto di quali thread acquisiscono lock, su quali variabili e in quale ordine.

Cercare di considerare sempre ogni eventualità.

 

 

IMPLENTARE I THREAD IN JAVA

E' possibile implementare i thread in Java principalmente in due modi:

 

  • estendendo la CLASSE ‘Thread';

  • implementando l'INTERFACCIA ‘Runnable'.

 

In effetti, esiste un terzo metodo, che consiste nel creare classi al volo all'interno di altre classi per eseguire dei thread ad hoc; una soluzione del genere rende il codice difficile da leggere e da capire in fase di ricerca degli errori, per qui è da evitare.

 

 

ESTENDERE LA CLASSE THREAD

Per estendere la classe Thread, scrivere semplicemente ‘extends Thread' durante la dichiarazione della classe e sovrascrivere il metodo run() : void.

Le azioni da effettuare durante l'esecuzione del thread andranno messe quindi nel corpo del metodo run.

Il thread viene attivato invocandone il metodo start().

Esempio: scrivo una classe MioThread...

 

public class MioThread extends Thread

{

public MioThread()

{

// Costruttore

}

public void run()

{

System.out.println(Thread in esecuzione);

}

}

 

e la creo ed avvio così: MioThread mioThread1 = new MioThread(); mioThread1.start(); .

A questo punto ci sono, in esecuzione, due thread per lo stesso programma: il thread che gestisce il main del programma (che ha creato ed avviato mioThread1, e continua la sua esecuzione) e mioThread1, che fa quello che gli diciamo di fare.

 

 

IMPLEMENTARE L'INTERFACCIA RUNNABLE

Se non vogliamo creare una classe ad hoc per il thread, ma vogliamo che un'altra classe (che magari eredita già da un'altra) si comporti come una classe Thread, possiamo implementare l'interfaccia runnable.

Anche in questo caso sovrascriveremo il metodo run(), mentre nel costruttore della classe creeremo un oggetto Thread passandogli, come parametro, l'oggetto che lo sta creando; ad esempio:

 

public class MiaClasse2 extends MiaClasse1 implements Runnable

{

public MiaClasse2

{

// Costruttore

Thread mioThread1 = new Thread(this);

mioThread1.start();

}

public void run()

{

System.out.println(Thread in esecuzione);

}

}

 

 

 

LA SINCRONIZZAZIONE DEI THREAD IN JAVA

Del perchè dover sincronizzare abbiamo parlato precedentemente, nella parte ‘teorica'... passiamo all'implementazione in Java.

In Java si fa uso della parola chiave synchronized; ad esempio, per rendere synchronized un metodo:

 

public synchronized int getValoreX()

{

return x;

}

 

o:

public synchronized void setValoreX(int in)

{

x = in;

}

 

E' possibile sincronizzare l'oggetto sul quale si lavora, al costo di qualche istruzione in più nel bytecode.

Il lato buono è che, con questo metodo, è possibile sincronizzare solo una parte di un metodo e non necessariamente tutto il metodo; ad esempio potremmo scrivere:

 

public void setValoreX(int in)

{

System.out.println(Sto per entrare nel blocco sync);

synchronized(this)

{

System.out.println(Dentro il blocco sync);

x = in;

}

System.out.println('Sono uscito dal blocco sync, ma non ancora dal metodo setValoreX');

}

 

E' possibile effettuare lock su qualsiasi oggetto, anche sugli array, ma non sui tipi primitivi.

 

 

METODI DI SINCRONIZZAZIONE: WAIT, NOTIFY e NOTIFYALL

 

wait() blocca l'esecuzione del thread invocante fino a che un altro thread invoca una notify() sull'oggetto.

Si fa sempre dopo aver testato una condizione (ed in un ciclo, per essere sicuri che al risveglio la condizione è verificata).

Esempio:

 

 

while (! condition) // se non pu procedere

{

this.wait (); // aspetta una notifica

}

 

Il thread invocante viene bloccato, il lock sull'oggettoè rilasciato automaticamente, mentre i lock su altri oggetti sono mantenuti (bisogna quindi fare attenzione a possibili deadlock).

La variante wait(long timeout) blocca il thread per al massimo timeout millisecondi (se timeout 0).

 

notify() risveglia un solo thread tra quelli che aspettano sull'oggetto in questione.

Se più thread sono in attesa, la scelta di quale thread svegliare viene fatta dalla JVM.

Una volta risvegliato, il thread compete con ogni altro (non in wait) che vuole accedere ad una risorsa protetta.

Non è un buon metodo di risveglio perchè il thread risvegliato, scelto dalla JVM, potrebbe non essere in grado di proseguire.

 

notifyAll() risveglia tutti i thread che aspettano sull'oggetto in questione.

Da preferire rispetto a notify, quando possibile.

 

 

ALTRE OPERAZIONI INTERESSANTI POSSIBILI CON I THREAD IN JAVA   ---   PAUSA

Possiamo mettere in pausa un Thread per un pò di tempo con la seguente riga di codice:

 

Thread.sleep([tempo in millisecondi]); .

 

 

JOIN

Se vogliamo che un thread attenda che un altro thread termini la sua esecuzione per avviarsi, utilizziamo la seguente istruzione:

 

myThread.join(); .

 

 

INTERRUPT

Il metodo interrupt() permette di interrompere l'esecuzione del thread sul quale si invoca, solo quando lo stato dell'oggetto lo consente, cioè quando non è in esecuzione, ma in attesa di un evento (in modo da mantenere lo stato consistente).

 

 
Vai all'inizio della pagina