class Conto {
private int soldiDisponibili = 50;
public int getSaldo() {
return soldiDisponibili;
}
public void prelievo(int cifraDaPrelevare) {
soldiDisponibili = soldiDisponibili - cifraDaPrelevare;
}
}
Abbiamo una classe Conto che rappresenta un normale conto in
banca. Ipotizziamo che due persone Tizio e Caio possono accedere allo stesso
conto. Cosa succede se Tizio visiona il conto e effettua un prelievo e nello
stesso momento Caio visiona il conto ed effettua un prelievo da qualche altra
banca? Entrambi pensano che nel conto ci siano soldi a sufficienza, ma, in
realtà il doppio prelievo porterebbe ad un valore negativo del conto.
L’esempio è molto banale. In genere, in Java si parla di
thread multipli (Tizio e Caio) che vogliono accedere alla stessa risorsa (conto
in banca) e possono produrre dei dati corrotti.
In questi casi il concetto di insieme di operazioni, cioè la
visione del conto e il prelievo, viene confinato in quel che si chiama
“operazione atomica”. Java non permette
di eseguire un insieme di operazioni una dopo l’altra senza interruzione, ma
permette che un qualsiasi Thread che esegue delle operazioni su un dato anche
se non è più in stato Running riesce a mantenere l’esclusività su quel dato, in
maniera tale che nessun altro Thread può accedervi. Tutto questo è permesso attraverso la parola
chiave Synchronized.
Quando si implementano classi che prevedono un accesso
concorrente bisogna assicurarsi che le variabili siano private e il codice che
le modifica sia marcato con la parola chiave Synchronized, il cui utilizzo sarà
più chiaro a breve. Nel caso del nostro esempio potremmo tradurlo in codice nel
seguente modo:
public synchronized void prelievo(int cifraDaPrelevare) {
soldiDisponibili = soldiDisponibili - cifraDaPrelevare;
}
Con la parola chiave indicata siamo sicuri che solo un
Thread alla volta può fare un prelievo sullo stesso conto. Due Thread diversi
possono comunque accedere al metodo simultaneamente se si riferiscono a due
oggetti diversi. Questo introduce il concetto di Locks.
Locks
La sincronizzazione si basa su un meccanismo molto semplice.
Ogni oggetto ha un meccanismo di Lock che entra in gioco quando vi è del codice
con la parola chiave synchronized. Quando si entra in un metodo non-static
synchronized, si acquisisce automaticamente un lock associato all’istanza su
cui si sta agendo. Poiché esiste un solo lock per un singolo oggetto, se un
thread acquisisce un lock, nessun altro thread può acquisirlo fino a che viene
rilasciato dal primo. Ciò vuol dire che il secondo thread non riuscirà ad
eseguire il metodo synchronized fino a quando il lock viene rilasciato.
Quest’ultimo evento avviene quando un thread finisce l’esecuzione del codice
synchronized.
In merito alla sincronizzazione e i locks vi sono alcuni
concetti importanti da tenere a mente:
- Solo i metodi o blocchi di codice possono essere synchronized. Questa parola chiave non è utilizzabile con classi o variabili.
- Ogni oggetto ha solo un lock.
- Non tutti i metodi in una classe devono essere sincronizzati.
- Fissato un istante di tempo, se due Thread vogliono accedere ad un metodo synchronized di uno stesso oggetto, solo uno sarà in grado di eseguire il thread. L’altro Thread non potrà accedere a NESSUN metodo synchronized dell’oggetto.
- Nel caso di metodi non-synchronized l’accesso è consentito anche in contemporanea con altri metodi synchronized.
- Se un Thread va in Sleep, esso mantiene comunque i lock che ha acquisito.
- Un Thread può acquisire più di un lock.
- Si possono sincronizzare anche blocchi di codice.
Synchronized
Block
Java consente al programmatore di ridurre l’impatto del
synchronized consentendo di utilizzarlo per solo blocchi di codice. Facciamo un
esempio:
class SyncTest {
public void avviaMetodo() {
System.out.println("non
synchronized");
synchronized(this) {
System.out.println("synchronized");
}
}
}
Quando si sfrutta il synchronized block, è necessario
indicare su quale oggetto si vuole acquisire il lock. In questo modo è
possibile indicare anche oggetti esterni. Nell’esempio specificato sfruttiamo
l’operatore this per acquisire il lock sull’oggetto stesso.
Synchronized
Static
Abbiamo detto che la parola chiave synchronized non può
essere utilizzata per variaibli o classi, ma solo per metodi o blocchi di
codice. Chiaramente, ciò comprende anche i metodi statici. Trattiamo questo
argomento in maniera separata poiché il meccanismo di lock è leggermente
diverso. Java contempla un lock per l’intera classe. Di conseguenza, qualsiasi
accesso ad un metodo synchronized determina un lock su tutti i metodi statici
della classe. Per il resto valgono le stesse regole già viste per gli oggetti.
Thread che
non possono acquisire il Lock
Se un Thread prova ad entrare in un metodo synchronized e il
lock è già stato acquisito, esso rimarrà bloccato sull’oggetto. In parole
povere, il Thread andrà a finire in un’area apposita e rimarrà in attesa che il
lock sia rilasciato. E’ importante tenere presente che non vi è nessuna
garanzia sull’esecuzione di un thread
piuttosto di un altro, dove entrambi sono in attesa su di un lock di un
oggetto. Inoltre:
- I Thread che richiamano metodi synchronized (non statici) nella stessa classe bloccano altri solo se la chiamata avviene sulla stessa istanza. Ciò è una conseguenza del fatto che ogni oggetto ha il proprio lock. Due oggetti possono essere utilizzati simultaneamente senza interferire l’uno con l’altro.
- I Thread che richiamano metodi synchronized statici sulla stessa classe si bloccheranno sempre l’uno con l’altro poiché il lock è unico per l’intera classe.
- I metodi synchronized non statici e statici non interferiscono l’uno con l’altro.
Quando
utilizzare la sincronizzazione
La sincronizzazione è un aspetto molto importante in Java
soprattutto quando si lavora con diversi Thread. Generalmente, quando in
qualsiasi istante più di un thread può accedere e cambiare dei dati, l’utilizzo
della sincronizzazione è altamente consigliato. Tuttavia, vi sono degli aspetti
da valutare perché abusare di questo meccanismo può portare a degli inutili
rallentamenti.
Per quanto riguarda le variabili locali, ogni Thread ha la
propria copia. Due Thread che eseguono lo stesso metodo allo stesso istante
utilizzeranno due differenti copie di variaibli locali. Per questo motivo non
bisogna preoccuparsi delle variabili locali. Allo stesso modo, non bisogna
preoccuparsi di variabili di istanza statiche o non statiche se queste
contengono dei dati che non vengono modificati. Infine, bisogna fare attenzione
all’accesso delle istanze. Sue due thread accedono a differenti istanze il
codice sincronizzato non è necessario.
0 commenti:
Posta un commento