La comunicazione tra thread – Lezione 41 di Java Avanzato

Sincronizzazione tra thread: la parola chiave synchronized

Dato che i thread di uno stesso processo condividono la memoria, risulta molto utile un meccasismo che consente di far comunicare i thread tra loro. Il modo più semplice per far comunicare i thread consiste nell’utilizzare degli oggetti condivisi.

Accedendo contemporaneamente agli stessi oggetti, due o più thread possono procurare effetti incontrollati sullo stato degli oggetti. Ad esempio all’atto della sospensione, il thread potrebbe impegnare una risorsa in modo esclusivo che potrebbe generare situazioni inconsistenti o di blocco critico (deadlock). Per garantire che un solo thread alla volta tenti di modificare uno stesso oggetto occorre sincronizzare i thread consentendo l’accesso alla risorsa ad un thread alla volta, questa proprietà prende il nome di mutua esclusione.
In Java, ad ogni oggetto è associato un mutex (semaforo binario) che consente di sapere se una risorse è libero o meno. Un mutex supporta le operazioni base di lock e unlock che consentono rispettivamente di bloccare e sbloccare l’accesso esterno ad una risorsa. A ogni oggetto oltre ad un mutex è associata anche una lista d’attesa che contiene le richieste d’accesso, non ancora evase, a quella risorsa.

La parola chiave synchronized permette di utilizzare implicitamente i mutex. Il programmatore ha il compito di individuare le sezioni di codice che operano su un oggetto come sezioni critiche ed inserire la parola chiave synchronized.
Il compilatore, in corrispondenza della sezione critica con synchronized inserisce in automatico una intestazione ed un epilogo in modo da implementare il “lock” associato all’oggetto riferito dalla sezione critica.

Metodi sincronizzati con la parola chiave synchronized

E’ possibile definire alcuni metodi synchronized in modo da assicurare la mutua esclusione:

  public synchronized int f() {
    // Sezione critica sincronizzata
  }

Quando si inserire la parola chiave synchronized nell’intestazione di un metodo equivale a dire:

chiedi il mutex all’inizio del metodo e rilascialo soltanto alla fine dello stesso

  public synchronized int f() {
    // Lock del mutex di this
    // Sezione critica
    // Unlock del mutex di this
  }

Con un metodo static non esiste nessun riferimento a this, non è possibile far riferimento al mutex della classe. Un metodo static quando utilizza la parola chiave synchronized equivale ad utilizzare il mutex dell’oggetto Class associato.

Quando un metodo tenta di acquisire un mutex, se quest’ultimo risulta già impegnato il thread viene messo in attesa che si liberi. Quando il thread che ha il mutex lo rilascia, il nostro thread potrà acquisire il mutex in questione.

Blocchi sincronizzati con synchronized(Object)

E’ possibile individuare anche piccole sezioni di codice critiche, in questo caso invece di utilizzare un intero metodo, utilizzeremo un blocco sincronizzato. Queste sezioni di codice sincronizzati richiedono come argomento l’oggetto del quale vogliamo acquisire il mutex. Ad esempio:

  synchronized(n) {
    // Sezione critica sincronizzata
  }

Per il compilatore, il codice appena scritto equivale a dire:

  // Lock del mutex associato a n
  // Sezione critica
  // Unlock del mutex associato a n

Anche per i blocchi sincronizzati, se un thread sta eseguendo un blocco sincronizzato su un oggetto x, gli altri thread dovranno aspettare finchè il mutex dell’oggetto x non venga liberato.

Mutex rientranti e classi Thread Safe

In Java, i mutex sono rientranti (reentrant). Un mutex rientrante indica che ogni mutex ha al proprio interno un contatore che viene incrementato se il mutex viene acquisito (lock) da un thread e viene decrementato se rilasciato (unlock). Quando un mutex risulta libero il contatore contiene il valore zero. Questo meccanismo consente di far acquisire ad un thread più volte lo stesso mutex. Se i mutex non fossero rientranti, un metodo sincronizzato che ne chiamasse un altro andrebbe automaticamente in deadlock. In Java un thread non può acquisire né rilasciare un mutex che in quel momento risulti acquisito (una o più volte) da un altro thread.

Una classe è definita Thread Safe quando la si può utilizzare da diversi thread senza preoccuparsi della corretta sincronizzazione che può causare problemi del tutto inaspettati, come vedremo con il Java Memory Model. Per ragioni progettuali tutte le classi di Collection non sono Thread Safe ad eccezione di alcune che vedremo in seguito. Le ragioni di questa scelta sono dovute al fatto che la maggior parte dei programmi sono single thread e risulterebbe inutile far acquisire i mutex alle collezioni per ogni add, remove, etc., introdurremmo un consistente overhead peggiorando i livelli di performance inutilmente.

Thread Pools. Java fornisce dei meccanismi (classi Executors) per gestire i thread pool. In questo modo il numero di thread creati può essere controllato in modo da controllare la disponibilità di risorse:

  • newFixedThreadPool, consente di creare un pool di thread che utilizza un numero fisso di thread;
  • newCachedThreadPool, i thread vengono creati quando necessario e mantenuti in idle per 60 secondi;
  • newSingleThreadExecutor, rappresenta un pool costituito da un solo thread che svolge i suoi compiti sequenzialmente;
  • newScheduledThreadPool, è un pool fisso per esecuzioni di thread schedulate;
  • newSingleThreadScheduledExecutor, rappresenta un singolo thread per l’esecuzione schedulata.
Indice Lezione PrecedenteLezione Successiva

Pubblicato in Guide, Java, Programmazione Taggato con: , , , ,

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*