La sincronizzazione permette di evitare l’esecuzione contemporanea di parti di codice delicate (Problemi di Competizione).
Realizazzione di sezioni critiche
Il multithreading può essere sfruttato per risolvere anche Problemi di Cooperazione (esecuzione di attività comune mediante scambio di informazioni).
Es: produttore-consumatore
Java fornisce il meccanismo di sincronizzazione dei mutex (contrazione di mutual exclusion) per l’accesso a sezione critiche.
Un mutex è una risorsa del sistema che può essere posseduta da un solo thread alla volta.
Ogni istanza di qualsiasi oggetto ha associato un mutex.
Il mutex di un qualsiasi oggetto non può essere acceduto direttamente dall’applicazione.
Uso di:
Quando un thread esegue un metodo/blocco che è stato dichiarato sincronizzato:
Metodo sincronizzato:
synchronized void Method(){...}
L’accesso al metodo è effettuato solo quando il lock associato all’oggetto è stato acquisito.
I metodi sincronizzati garantiscono l’accesso in mutua esclusione ai dati incapsulati in un oggetto solo se i dati sono accessi da altri metodi definiti synchronized.
Dove
public synchronized int read(){
richiede il monitor dell'istanza di oggetto e lo blocca a inizio del metodo; ereturn theData; }
rilascia il monitor dell'istanza di oggetto e sblocca eventuali thread in attesa.I metodi non sincronizzati non richiedono il lock e possono essere eseguiti in ogni istante senza garanzie di mutua esclusione.
Per ottenere la piena mutua esclusione, tutti i metodi che accedono a dati incapsulati nell'oggetto devono essere dichiarati come synchronized.
Java offre la possibilità di definire sezioni critiche anche quando queste non coincidono con il corpo di un metodo attraverso i cosiddetti blocchi sincronizzati.
La parola chiave synchronized
prende come parametro il riferimento ad un oggetto del quale si deve ottenere il lock per continuare
public int read(){
synchronized(this) //oggetto corrente
{
return theData;
}
}
Maggiore espressività nell’implementare vincoli di sincronizzazione di un programma.
Responsabilità del chiamante per la sincronizzazione, non del chiamato.
L’eccessivo uso di blocchi sincronizzati può rendere il codice disordinato.
I vincoli di sincronizzazione non sono più incapsulati in un singolo posto (es. definizione di un metodo)
Non è possibile comprendere i vincoli di sincronizzazione associati ad un oggetto “O” guardando l’oggetto “O”.
Bisogna guardare a tutti gli oggetti che accedono ad “O” in un blocco synchronized.
Java consente ad un thread di acquisire un lock anche se il lock è stato già acquisito dal thread.
Possibilità di overlap tra metori e blocchi synchronized.
Il programmatore non ha la preoccupazione di rilasciare il mutex ogni volta che un metodo termina normalmente o a causa di una eccezione, viene eseguito automaticamente.
Es: un insieme di thread che accede in lettura e/o scrittura ad una variabile in maniera concorrente:
Se un thread ha invocato check(), deve avere la certezza che nessuno altro possa usare la risorsa prima che possa eseguire una read() o una write().
Implementazione con soli blocchi synchronized
class Wrapper
{
private int buffer;
.
.
.
synchronized void write() {. . .}
synchronized void read() {. . .}
synchronized bool check() {. . .}
}
Non è corretta dal momento che un thread può acquisire il monitor rilasciato da un altro all’uscita di check(), andando a violare il requisito di atomicità della coppia check() + read()/write().
Dove
synchronized void write() {. . .}
synchronized void read() {. . .}
sono le operazioni di scrittura e lettura in mutua esclusione.
E’ più corretto ricorrere ai blocchi sincronizzati.
Mostra codiceNella classe wrapper non adottiamo nessuna particolare misura di sincronizzazione:
void write() {. . .}
void read() {. . .}
bool check() {. . .}
Implementiamo anche una classe MyRead, sottoclasse di Thread, che nel metodo run() invoca read(), invece di write().
Mostra codiceNel metodo run() di ogni Thread realizziamo un blocco sincronizzato usando il monitor associato all'istanza di Wrapper:
synchronized(wrapper) {
. . .
if(check())
write(); }
Dove con l'istruzione Wrapper buf = new Wrapper();
istanzio un oggetto della classe Wrapper.
Con il comando
th = new MyReader(buf);
is_reader = false;
} else {
th = new MyWriter(buf);
istanzio degli oggetti delle classi MyReader e MyWriter passando sempre lo stesso oggetto di Wrapper, così che tutti i thread abbiano i relativi bloc-chi sincronizzati sullo stesso monitor.
Anche i metodi statici possono essere dichiarati sincronizzati poiché essi non sono legati ad alcuna istanza, viene acquisito il mutex associato all’istanza della classe Class che descrive la classe.
Se invochiamo due metodi statici sincronizzati di una stessa classe da due threads diversi essi verranno eseguiti in sequenza.
Se invochiamo un metodo statico e un metodo di istanza, entrambi sincronizzati, di una stessa classe essi verranno eseguiti in concorrenza.
Class StaticSharedVariable{
...
Public int Read(){
Synchronized (StaticSharedVariable.class){
Return shared;
}
}
Public synchronized static void Write (int I){
Shared = I;
}
Private static int shared;
}
Ottenere il lock del Class Object non influenza I lock di qualsiasi istanza della classe. Sono indipedenti.
Se una classe non ha metodi sincronizzati ma si desidera evitare l’accesso contemporaneo a uno o più metodi è possibile acquisire il mutex di una determinata istanza racchiudendo le invocazioni dei metodi da sincronizzare in un blocco sincronizzato.
Struttura dei blocchi sincronizzati
private Object mioLock=new Object();
synchronized (mioLock) {
comando 1;
...
comando n;}
In generale non è possibile determinare I lock posseduti da un thread, ma viceversa, è possibile determinare se un thread detiene il lock di un particolare oggetto.
Classe Thread
:
Public static boolean holdsLock(Object obj)
In java 1.5 è anche possibile determinare lo stato corrente di un thread al runtime: metodo getState()
della classe Thread
.
Ritorna uno dei possibili stati
BLOCKED,NEW,RUNNABLE,TERMINATED,TIMED_WAITING,WAITING
È la tipologia di monitor utilizzato dalla JVM:
Invocabili solo dall’interno dei metodi che possiedono il lock sull’oggetto. Se invocati dall’esterno, viene sollevata IllegalMonitorStateException
.
Il thread attivo nel monitor può sospendere la sua esecuzione invocando la primitiva wait();
Quest’ultima ha l’effetto di rilasciare il monitor ed inserire il thread nel “wait set”.
Il thread rimarrà nel “wait set” finchè non verrà invocata una notify()
da un altro thread che è attivo nel monitor.
Quando un thread esegue la notify()
può sorgere un problema
Come conseguenza della notify entrambi i thread possono, concettualmente, proseguire, violando le proprietà di un monitor!
Uno dei due thread deve essere sospeso.
Tale soluzione prevede che il thread Q continui l’esecuzione e che P venga riattivato non appena Q rilascia il monitor o completa la sua esecuzione.
La soluzione adottata in Java è spesso riferita in letteratura come “Signal and Continue” monitor poiché, differentemente dalla soluzione di Hoare (cfr. Sis. Op.), il thread che invoca la notify() rimane in possesso del monitor e continua la sua esecuzione all’interno della monitor region.
Il thread che ha eseguito la notify()
continuando l’esecuzione potrebbe alterare una variabile condivisa con il thread su cui si è invocata la notify()
.
Il thread che viene “svegliato” dalla notify()
potrebbe così operare su uno stato non consistente (statement di Hoare).
Esempio: un problema di produttori consumatori in cui uno produttore dopo aver invocato una notify() altera il contenuto del messaggio depositato.
public final void wait( )
Il thread che invoca questo metodo entra nel wait set del monitor, rilasciando il mutex associato all’istanza e rimane sospeso fintanto che non viene risvegliato da un altro thread che invoca il metodo notify
o notifyAll
, oppure viene interrotto con il metodo interrupt della classe Thread
.
public final void wait (long millis)
Si comporta analogamente al precedente, ma se dopo un’attesa corrispondente al numero di millisecondi specificato in millis non è stato risvegliato, esso si risveglia.
public final void wait (long millis, int nanos)
Si comporta analogamente al precedente, ma permette di specificare l’attesa con una risoluzione temporale a livello di nanosecondi.
public final void notify ( )
public final void notifyAll ( )
Le condition variables sono utilizzate qualora si voglia eseguire una sezione critica solo nel caso in cui sia verificata una certa condizione R.
Tali variabili permettono di testare all’ingresso di una sezione critica una certa condizione R:
La coppia costituita da wait()
e notify()
(notifyAll()
), consente di implementare le condition variables.
Avvertenze per le condition variables con wait() e notify()
Al risveglio, un thread non può assumere che la sua condizione sia vera dato che tutti i threads potrebbero essere risvegliati indipendetemente dalla condizione sulla quale erano in attesa
Per alcuni algoritmi questa limitazione non è un problema, dato che le condizioni sulle quali I thread sono in attesa possono essere mutuamente esclusive
E.g., produtture – consumatore: 2 condition variables: spazioDisponibile e messaggioDisponibile.
Un thread produttore è in attesa di spazioDisponibile e se risvegliato da un thread (solo un consumatore può ) è sicuro che la sua condizione sia valida.
Ma questo non è il caso generale! Java non da garanzie che un thread risvegliato da una wait possa immediatamente acquisire il lock.
Un altro thread produttore potrebbe acquisire il lock prima e riempire di nuovo il buffer.
Quando l’altro thread acquisisce il lock, la condizione spazioDisponibile non è più verificata!
E’ assolutamente essenziale che un thread verifichi la sua condition variable non appena risvegliato!
L’esecuzione procede solo se la condizione è verificata, altrimenti il Thread si pone in attesa:
while(!obj.my_condition) {
obj.wait();
}
Thread 3 Mostra codice
Quando un thread abilita la condizione, sblocca anche uno dei thread che era stato posto in attesa per l'esecuzione nel metodo wait().
obj.my_condition = true;
obj.notify();
//obj.notifyAll();
I metodi wait() e notify() Trattandosi di primitive di cooperazione non risolvono la mutua esclusione, quindi vanno combinati ai monitor per evitare accessi spuri alle risorse condivise.
Es. Thread che scrivono in un buffer di capacità limitata.
Il thread produttore che viene risvegliato deve controllare subito se la condizione di risveglio è effettivamente quella di interesse (spazioDisponibile).
A partire da Java 1.5 di Java, sono stati introdotti ulteriori costrutti per la gestione della sincronizzazione.
Tre packages:
java.util.concurrent
- fornisce le classi per supportare paradigmi comuni di programmazione concorrente: queuing policies come bounded buffers, set, maps, thread pools ecc.java.util.concurrent.atomic
- fornisce un supporto per una programmazione lock-free e thread-safe su variabili semplici come atomic integer, atomic boolean, ecc.java.util.concurrent.locks
- fornisce un framework per diversi algoritmi di locking the completano il meccanismo base fornito da Java. Es., read -write locks e condition variables.Il package java.util.concurrent.atomic fornisce le classi per l’accesso (set, get, getAndSet, compareAndSet, weekCompareAndSet) a singole variabili in modalità atomica: AtomicBoolean, AtomicInteger, AtomicIntegerArray. . .
Es. metodo di aggiornamento di AtomicInteger
boolean compareAndSet (int expect, int update): Assegna atomicamente update se il valore corrente è uguale a expect. Restituisce true se aggiorna con successo, false se il valore corrente era diverso da quello atteso.
Class InteroAtomico {
private int val;
synchronized int get()
{ return val; }
-> class AtomicInteger {. . .}
synchronized void set(int_val)
{ val = _val; }
. . . }
La classe Semaphore gestisce un insieme di permessi.
acquire()
eseguito sul semaforo blocca il thread corrente se sul semaforo non c’è almeno un permesso disponibile da acquisire.release()
aggiunge un permesso al semaforo, potenzialmente sbloccando un thread bloccato in fase di acquisizione.I semafori vengono utilizzati generalmente quando si vuole restringere il numero di thread che possono accedere ad una determinata risorsa condivisa.
NB: sebbene il semaforo incapsula la sincronizzazione necessaria a restringere l’accesso alla risorsa, la consistenza della risorsa va comunque gestita a parte.
Un semaforo inizializzato ad “1″ ed usato in modo da non avere più di “1″ permesso disponibile, funge da lock per la mutua esclusione (semaforo binario).
A differenza di molte implementazioni di Lock (presenti nel package java.util.concurrent.locks), il lock effettuato col semaforo binario può essere rilasciato da un qualsiasi altro thread.
Il costruttore della classe Semaphore ha come parametro il numero di permessi iniziale, inoltre ha un parametro opzionale boolean per imporre l’ordinamento FIFO (true) o non garantire alcun ordinamento (false o non specificato):
Istanzio un oggetto semaforo con numero di permessi pari a MAX_AVAIL e con politica di accodamento FIFO:
private Semaphore available = new Semaphore(MAX_AVAIL, true);
Acquisisco un per,esso sul semaforo, tale metodo può sollevare eccezione di tipo InterruptException (come già visto in merito a wait()
)
available.acquire();
Rilascio un permesso sul semaforo, tale metodo non solleva alcun tipo di eccezione (come già visto in merito a notify()
)
available.release();
Necessarie per raggiungere un punto comune di esecuzione all’interno dei vari thread concorrenti per effettuare delle precise operazioni e poi ripartire.
La classe CyclicBarrier permette a un insieme di thread di aspettare ognuno il raggiungimento da parte di tutti gli altri di un punto comune (barriera) e di eseguire un’azione dopo che l’ultimo thread si è messo in attesa e prima che qualsiasi thread sia rilasciato.
Questa classe si chiama ciclica perché la barriera può essere riutilizzata anche dopo che tutti i thread che aspettavano sono rilasciati.
Istanzio un oggetto CyclicBarrier passando il numero di thread che devono sincronizzarsi sulla barriera e un oggetto che implementa Runnable, quest'ultimo realizzato come classe interna anonima:
barrier = new CyclicBarrier (N, new Runnable() {
public void run() {
mergeRows(...);
}
});
Lock è una interfaccia del package java.util.concurrent.
Le sue implementazioni offrono un uso più flessibile rispetto ai lock associati ai blocchi o metodi synchronized.
Permettono l’accesso esclusivo ad una risorsa ma a differenza del lock associato alla synchronized permette di rilasciare i lock acquisiti in un qualsiasi ordine (con la synchronized deve essere rispettato l’ordine inverso a quello di acquisizione).
Lock l = ...;
l.lock();
try {
// accesso alla risorsa protetta da questo lock
} finally {
l.unlock(); //garantire il rilascio della risorsa
}
Le implementazioni di Lock offrono funzionalità addizionali oltre a quelli fornite dall’uso dello statement synchronized.
Un tentativo non bloccante di acquisire il lock (tryLock()
).
Un tentativo di acquisire il lock che può essere interrotto (lockInterruptibly()
) un altro thread può interromperlo invocando il metodo interrupt() della classe Thread.
Un tentativo di acquisire il lock che può essere interrotto da timeout (tryLock(long, TimeUnit)).
La classe ReentrantLock
è un Lock con lo stesso comportamento e semantica dei lock impliciti associati ai metodi e blocchi synchronized ma appartiene al solo thread che lo ha acquisito con successo e non ancora rilasciato.
I Lock vengono adoperati per la gestione dell’accesso in mutua esclusione, ma offrono meccanismi anche per la cooperazione tra thread attraverso implementazioni dell’interfaccia Condition.
Condition fornisce un mezzo per un thread di sospendere l’esecuzione fino a quando è notificato da un altro thread che una certa condizione di stato è ora vera.
Gli oggetti di tipo Condition consentono di implementare le condition variables.
Poiché l’accesso a queste informazioni di stato è condiviso tra diversi thread, esso deve essere protetto, per cui un lock è associato alla condizione.
Un’istanza di tipo Condition è intrinsecamente legata a un Lock. Per ottenere un’istanza Condition per una particolare istanza di Lock bisogna usare il suo metodo newCondition()
.
Istanzio un oggetto della classe ReentrantLock: final Lock lock = new ReentrantLock();
Ottengo due oggetti Conditions dal Lock:
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
Obbligo il chiamante a gestire le eccezioni sollevabili dalla wait: throws InterruptedException {
Proteggo l'accesso in sezione critica e mi metto in attesa che la condizione sia vera:
lock.lock();
try {
while (count == items.length)
notFull.await();
Abilito la seconda condizione e sblocco il Lock:
notEmpty.signal();
} finally {
lock.unlock();
Metodo take() che acquisisce il lock, controlla la condizione e poi sblocca il lock:
Mostra codice
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS
Bruce Eckel, “Thinking in Java” capitolo 13