Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
I corsi di Ingegneria
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Stefano Russo » 8.Sincronizzazione tra thread


Sommario

  • Sincronizzazione, Mutua Esclusione e Cooperazione
  • Monitor
  • Condition Variables
  • Sincronizzazione in Java 1.5
    • Semaphore
    • Atomic Variables
    • Lock
    • Barriere

Comunicazione tra thread

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

  • il thread consumatore deve attendere che i dati da utilizzare vengano prodotti;
  • il thread produttore deve essere sicuro che il consumatore sia pronto a ricevere per evitare perdita di dati.

Sincronizzazione

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:

  • metodi sincronizzati;
  • blocchi sincronizzati.

Sincronizzazione

Quando un thread esegue un metodo/blocco che è stato dichiarato sincronizzato:

  • Entra in possesso del mutex associato all’istanza;
  • il thread che blocca il mutex (“mutex lock”) acquisisce l’accesso esclusivo alla sezione critica;
  • eventuali thread che vogliano accedere alla sezione critica saranno posti in una stato di attesa (blocked).

Synchronized

Metodo sincronizzato:

  • anteponendo la parola chiave synchronized alla firma del metodo;
  • 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.

Synchronized

Mostra codice

Dove

  • public synchronized int read(){ richiede il monitor dell'istanza di oggetto e lo blocca a inizio del metodo; e
  • return 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.

Blocchi 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;
}
}

Blocchi synchronized: avvertenze

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.

Vantaggi per il programmatore

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. sincronizzazione

Es: un insieme di thread che accede in lettura e/o scrittura ad una variabile in maniera concorrente:

  • un thread deve eseguire le seguenti operazioni in mutua esclusione:
    • check() – Controllo disponibilità;
    • write() / read() – Operazioni di scrittura / lettura.

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().


Es. sincronizzazione

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.

Es. sincronizzazione

E’ più corretto ricorrere ai blocchi sincronizzati.

Mostra codice

Nella 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 codice

Nel metodo run() di ogni Thread realizziamo un blocco sincronizzato usando il monitor associato all'istanza di Wrapper:

synchronized(wrapper) {
. . .
if(check())
write(); }

Es. sincronizzazione

Mostra codice

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.

Sincronizzazione metodi statici

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.

Sincronizzazione metodi statici

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.

Sincronizzazione implicita

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;}

Lock e stato di un thread

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

Monitor: richiami


Wait and notify monitor

È la tipologia di monitor utilizzato dalla JVM:

  • wait();
  • notify() – notifyAll().

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.

Wait and notify 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.


Soluzione di Java

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.


Signal and Continue

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.

Alcuni problemi

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.

wait()

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.

notify() – notifyAll()

public final void notify ( )

  • Risveglia un thread nel wait set del monitor.
  • Poiché il metodo che invoca notify deve aver acquisito il mutex, il thread risvegliato deve attenderne il rilascio e competere per la sua acquisizione come un qualsiasi altro thread.

public final void notifyAll ( )

  • Risveglia tutti i threads nel wait set del monitor.
  • I threads risvegliati competono per l’acquisizione del mutex e se ne esiste uno con priorità più alta, esso viene subito eseguito.

Condition variable

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:

  • se R==true il thread esegue il codice contenuto nella sezione critica;
  • altrimenti (R==false), il thread si mette in attesa di essere risvegliato, rilasciando la sezione critica;
  • opportunamente un thread può risvegliare altri thread che sono in attesa.

La coppia costituita da wait() e notify() (notifyAll()), consente di implementare le condition variables.

Condition variable

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.

Condition variable

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!

Cooperazione

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.

Bounded Buffer

Mostra codice

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).

Bounded Buffer

Mostra codice

Java 1.5 Concurrency Utilities

A partire da Java 1.5 di Java, sono stati introdotti ulteriori costrutti per la gestione della sincronizzazione.
Tre packages:

  1. java.util.concurrent - fornisce le classi per supportare paradigmi comuni di programmazione concorrente: queuing policies come bounded buffers, set, maps, thread pools ecc.
  2. java.util.concurrent.atomic - fornisce un supporto per una programmazione lock-free e thread-safe su variabili semplici come atomic integer, atomic boolean, ecc.
  3. 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.

Variabili Atomiche

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; }
. . . }

Semafori

La classe Semaphore gestisce un insieme di permessi.

  • Ogni acquire() eseguito sul semaforo blocca il thread corrente se sul semaforo non c’è almeno un permesso disponibile da acquisire.
  • Viceversa, una 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.

Semafori

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):

  • Semaphore(int permits);
  • Semaphore(int permits, boolean fair).

Semafori

Mostra codice

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();

Barriere

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.


Barriere

Mostra codice

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(...);
}
});

Barriere

Mostra codice

Il thread si pone in attesa sulla barriera:

barrier.await();

Lock

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
}

Lock

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.

Lock

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().

Lock

Mostra codice

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();

Lock

Metodo take() che acquisisce il lock, controlla la condizione e poi sblocca il lock:
Mostra codice

I materiali di supporto della lezione

Bruce Eckel, “Thinking in Java” capitolo 13

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion