+
Programmazione
concorrente in Java
+ Introduzione al multithreading
La scomposizione in oggetti consente di separare un programma in sottosezioni indipendenti.
Oggetto = metodi + attributi finalizzati alla rappresentazione di una specifica entità
Spesso non è sufficiente, è necessario ripartire il programma in sotto-attività da eseguire in parallelo.
Ciascuna sotto-attività può essere definita come thread
Componente software in esecuzione come “filo” indipendente all’interno della JVM
Non associati necessariamente a thread reali della macchina fisica.
2
+ Processi e Thread
Un processo è un programma software in esecuzione con accesso ad uno specifico spazio di indirizzamento in
memoria.
Un S.O. è in grado di gestire più processi allocando in maniera variabile il tempo di CPU a ciascun processo.
Un thread è un flusso di esecuzione all’interno di un processo.
Ciascun processo può avere a sua disposizione più thread in esecuzione parallela.
Perché i thread? Eseguire task ed utilizzare risorse senza bloccare l’esecuzione del programma principale.
E.g.: interfaccia grafica reattiva.
3
+ Thread in Java
In Java ciascun thread ha il proprio stack di esecuzione ed il proprio program counter
Condividono gli uni con gli altri il medesimo spazio di memoria per l’accesso agli oggetti.
Quando programmiamo utilizzando più thread in esecuzione contemporaneamente parliamo di programmazione
concorrente.
La JVM utilizza alcuni thread per le proprie attività “sotto il cofano”
Garbage Collector
Event Dispatcher
4
+ Thread in Java
5
Da Deitel & Deitel – “Java – How To Program”
Stati di un thread in Java
+ Thread in Java: gli stati
New: il thread è stato creato ma non è ancora in esecuzione;
Runnable: il thread è in esecuzione;
Waiting: l’esecuzione del thread A è stata sospesa in attesa dell’esecuzione di un altro thread B.
A può essere nuovamente in esecuzione solo al termine di B
Timed Waiting: similare allo stato waiting, ma non con la possibilità per il thread di rientrare in esecuzione al termine dell’intervallo del timer.
E’ possibile ad esempio forzare tutti i thread che per loro natura operano in polling sullo stato di una risorsa.
6
+ Thread in Java: gli stati
Blocked: il thread è bloccato ed escluso dall’esecuzione in attesa della disponibilità di una risorsa.
e.g.: IO da filesystem;
Terminated: il thread è escluso dall’esecuzione e non può rientrarvi in nessun modo.
Conclusione naturale del flusso di esecuzione
Conclusione forzata
Ciascun thread in Java ha un priorità di esecuzione, che ne regola il periodo di accesso alla CPU
Ereditato dal thread padre
Non è una garanzia dell’ordine di esecuzione dei thread stessi.
7
+ Thread in Java
In Java è possibile definire una attività ad esecuzione parallela mediante l’implementazione dell’interfaccia
java.lang.Runnable.
Definisce un unico metodo, run, che definisce la semantica del task da eseguire.
Per eseguire un oggetto Runnable è necessario disporre di un oggetto Executor.
Crea e gestisce un gruppo di thread.
Per ciascun oggetto Runnable il metodo run è invocato in un nuovo thread.
Il metodo execute(Runnable) aggiunge un oggetto Runnable al gruppo di thread e, ove vi siano thread disponibili, ne avvia
l’esecuzione invocando il metodo run.
8
+ Executor
E’ sempre preferibile utilizzare un oggetto della classe Executor per l’avvio di oggetti Runnable ad esecuzione concorrente.
La gestione di un pool di thread consente di ottimizzare l’esecuzione grazie al riuso di thread esistenti.
Riduzione significativa dell’overhead legato alla creazione di un nuovo thread.
E’ tuttavia possibile (in particolare nelle precedenti versioni di Java) utilizzare metodologie alternative per l’avvio dei Thread.
Ad oggi sono del tutto sconsigliabili e deprecate.
L’interfaccia ExecutorService estende Executor ed
aggiunge alcune particolari funzionalità per la gestione del ciclo di vita dell’esecutore.
9
+ Esempio
10
Metodo run
Pone il thread in stato timed waiting
+ Esempio
11
Creazione con metodo statico dell’esecutore
Blocca l’accettazione di nuovi task pur continuando l’esecuzione dei task esistenti fino al completamento
Il metodo main è eseguito nel MAIN THREAD della JVM !!!
+ Sincronizzazione
E’ spesso necessario sincronizzare più thread rispetto ad una risorsa.
E.g.: più thread che aggiornano la medesima risorsa condivisa.
Solo uno dei thread deve poter accedere, in modo esclusivo, ai metodi per l’aggiornamento della risorsa.
Gli altri thread passano in stato waiting fino al rilascio della risorsa da parte del thread in esecuzione.
Uno dei thread in attesa (la selezione del thread non è prevedibile) è avviato.
Mutua esclusione dei thread in modalità di esecuzione.
Vediamo come realizzare uno dei meccanismi di sincronizzazione più utilizzati nella programmazione concorrente: i monitor.
12
+ I monitor
Tutti gli oggetti in Java sono caratterizzati dalla presenza di un monitor interno e di un instrinsic lock (o monitor lock).
Un solo thread alla volta può acquisire il monitor lock di un oggetto.
Quando un thread deve eseguire una operazione potenzialmente critica per la concorrenza deve acquisire un monitor lock.
Nel caso in cui il monitor lock sia già stato acquisito da un altro thread, il thread chiamante passa in modalità blocked.
La definizione di parti di codice soggette al monitoraggio da parte del monitor si realizza mediante l’utilizzo dell aparola riservata synchronized.
13
+ I monitor
synchronized(oggetto){
codice da eseguire }
Al termine della esecuzione il thread rilascia il monitor lock ed uno dei thread in stato blocked rispetto al monitor lock può essere eseguito
Anche i metodi di istanza possono essere caratterizzati dalla presenza del modificatore synchronized
L’esecuzione dell’intero metodo è soggetta all’acquisizione del monitor lock.
14
Oggetto rispetto al quale il thread deve acquisire il monitor lock
Un solo thread alla volta eseguirà questa porzione di codice
+ Sincronizzazione
Definire le aree del codice il cui accesso debba essere soggetto all’attività di sincronizzazione
Definire le operazioni critiche, soprattutto su dati condivisi, come synchronized, cercando di renderle il più possibile atomiche.
Riuscire a definire operazioni atomiche la cui esecuzione non può essere svolta contemporaneamente da più thread.
E’ preferibile mantenere il più ridotte possibili le aree di codice soggette al modificatore synchronized per
mantenerne elevata l’efficienza.
Soprattutto su potenziali chiamate bloccanti, quali accesso a DB o IO, per le quali il thread può mantenere il monitor lock per lunghi intervalli di tempo.
15
+ Struttura dati condivisa non sincronizzata
16
+ Produttore
Aggiunge un valore casuale ad una struttura dati condivisa e me tiene copia localmente.
Al fine di garantire la sincronizzazione utilizzeremo una struttura dati di tipo Queue ad accesso sincronizzato, la ArrayBlockingQueue
Metodi put e take per la scrittura e la lettura dalla coda.
17
+ Produttore
18
Sospendo il thread per un periodo di tempo casuale inferiore ai 3
+ Consumatore
Legge un valore da una struttura dati condivisa.
Mantiene copia del valore localmente
19
Sospendo il thread per un periodo di tempo casuale inferiore ai 3 secondi
+ Esecuzione
Nel nostro sistema manca del tutto la sincronizzazione !
Infatti eseguendolo scopriamo che…
20
Il thread produttore ha generato un totale di elementi la cui somma è 29…
Ma il thread consumatore ne ha letti solo 27
Questi valori cambiano AD OGNI ESECUZIONE
+ Introduciamo una struttura dati sincronizzata
Rivediamo l’esempio precedente modificando l’implementazione interna della classe Valore.
In particolare adottiamo come strumento per la memorizzazione del dato condiviso un oggetto della classe
ArrayBlockingQueue
Coda (tipata) di oggetti di dimensione prefissata ad accesso sincronizzato.
Quando un thread cerca di scrivere su una coda piena, rimane bloccato in attesa della lettura di almeno un valore da parte del thread consumatore
Viceversa per il thread consumatore, che legge i valori dalla coda.
La lettura dei valori “consuma” il valore presente all’interno della coda.
21
+ Struttura dati condivisa sincronizzata
22
Chiamata sincronizzata in lettura
Chiamata sincronizzata in scrittura
Lasciamo inalterato il resto del programma
+ Esecuzione
I due thread risultano sincronizzati
23
Poiché la coda ha lunghezza 1 le due operazioni, scrittura e lettura, sono
necessariamente alternate fra loro Tuttavia è vero che
lettura e scrittura sulla coda non sono
atomiche rispetto alle attività di lettura e scrittura dei due thread
Intervallo fra esecuzione di take e di println
+ Ma come funziona
ArrayBlockingQueue ?
Impostare i metodi set e get come metodi sincronizzati, il cui accesso sia a tutti gli effetti atomico.
Utilizzare un monitor lock per definire quale thread possa effettuare le attività (in lettura o scrittura) sui dati condivisi.
Se un oggetto non può eseguire una attività su un oggetto rispetto al quale si sincronizza (e.g.: la coda è piena ed il
thread di scrittura non può pubblicare) dopo aver acquisito il monitor lock passa in waiting e rilascia il monitor lock.
Quando un thread in esecuzione completa una attività che può modificare una condizione bloccante per un altro thread in waiting, lo notifica.
24
+ Metodi wait e notify
Metodi della classe Object per la gestione della sincronizzazione:
wait: notifica al thread che sta eseguendo il codice che deve spostarsi in stato waiting.
notify: notifica ad uno dei thread in attesa che è possibile passare allo stato runnable.
notifyAll: notifica a tutti i thread in in attesa che è possibile passare allo stato runnable.
In questo caso uno dei thread è posto in esecuzione, con un processo di selezione casuale.
E’ possibile invocare uno dei tre metodi solo se il thread
chiamante ha acquisito il monitor lock sull’oggetto rispetto alla quale si sincronizza.
25
+ Modifichiamo la classe Valore
26
Blocca il thread rispetto all’oggetto
Attiva tutti gli altri thread Metodi
sincronizzati
+ Esecuzione
27
I due thread risultano sincronizzati
Siamo riusciti a simulare il
comportamento della struttura dati ad
accesso concorrente.
Importantissimo:
acquisite il monitor lock sull’oggetto prima di invocare wait…
+ E se tolgo i metodi synchronized ?
28
E siamo pure fortunati … avremo potuto avere una eccezione !!!
+ Ma con più di due thread ?
Il nostro programmino non funzionerebbe a dovere.
Perché?
Semplice, quando esco dal wait non verifico nuovamente la condizione di waiting
Ma se nel frattempo una terza entità l’avesse modificata di nuovo ?
Modifichiamo la classe Valore affinché il metodo wait sia contenuto all’interno di un ciclo di verifica della condizione.
Come ?
while(condizione){
wait();
}
29
+ Ulteriori considerazioni
Fin qui tutto bene se i nostri thread operano alla stessa velocità.
Ma se operassero a velocità differenti avremo una perdita di performance, poiché la nostra applicazione sarebbe vincolata alla velocità del thread più lento
Per questo motivo possiamo pensare di introdurre un buffer che consenta al thread più veloce di continuare a lavorare ad alta velocità
Teoricamente buffer di lunghezza 1 se il consumatore è molto più veloce del produttore
Buffer di lunghezza infinita nel caso contrario.
Cercare il giusto thread off fra spazio del buffer e tempo medio di waiting dei thread
30
+ Modifichiamo ancora la classe Valore
31
+ Esempio
32
Produttore più veloce del consumatore
Buffer di dimensione 10
+ Interfaccia Lock
Tutti gli oggetti in Java hanno al loro interno il riferimento al monitor lock, oggetto che implementa l’interfaccia
java.util.concurrent.locks.Lock
Il monitor lock può essere acquisito mediante accesso ad una sezione synchronized o mediante l’invocazione del metodo lock.
Il metodo unlock consente di rilasciare il monitor lock, consentendo l’acquisizione ad eventuali thread bloccati rispetto al monitor lock
La classe ReentrantLock implementa l’interfaccia Lock lasciando accedere al monitor lock il thread in attesa da più tempo.
Fairness policy
Nessun thread può rimanere bloccato all’infinito (starvation)
33
+ Condition
Se un thread in possesso di un monitor lock determina che non può eseguire il proprio compito, si dice che sta
attendendo una condizione su un oggetto.
L’oggetto Lock consente di definire le condizioni di attesa di un thread
Insieme delle condizioni che devono essere verificate per consentire al thread l’esecuzione delle proprie attività.
Acquisito il Lock, un thread può creare una nuova condizione sul lock grazie al metodo newCondition
Restituisce un oggetto che implementa l’interfaccia java.util.concurrent.locks.Condition
34
+ Condition
Per attendere rispetto ad un oggetto Condition, il thread invoca il metodo await dell’oggetto stesso.
Quando un thread ha completato la sua attività e ritiene di poter risvegliare un insieme di thread associati ad una condition, può invocare il metodo signalAll della condition stessa.
Posso teoricamente avere più condizioni sullo stesso monitor lock !!!
Molto più potenti rispetto al synchronize
Interruzione e timeout di attesa dei thread;
Condizioni multiple di attesa per un thread;
Nessuna necessità di acquisire e rilasciare il lock nel medesimo blocco di codice.
35
+ Modifichiamo ancora la classe Valore
36
+ Esempio
37