Gestione dei dati condivisi in Java
Gestione dei dati condivisi in Java
Possiamo pensare ad un dato condiviso come ad una qualsiasi informazione esposta ad attività di elaborazione da più parti. Immaginiamo ad esempio di memorizzare in una variabile numerica il valore che rappresenta il cambio euro/dollaro. Potremmo avere un programma che verifica periodicamente quale sia il tasso di cambio e aggiorna il valore della variabile. Uno o più altri programmi invece potranno servirsi del tasso di cambio aggiornato per operare conversioni di prezzo.
Notiamo come la gestione di un dato condiviso, anche in questo esempio molto semplice, necessiti una particolare attenzione. Immaginiamo ad esempio che, mentre un'applicazione richiede in lettura il tasso di cambio per effettuare la conversione, venga ricevuta anche, da parte di un altro programma, una richiesta di modifica del tasso di cambio stesso.
Quale viene eseguita per prima? Con quale tasso di cambio viene effettuata la conversione? Il problema, addirittura, in alcuni casi leggermente più complessi, potrebbe essere anche più grave.
Immaginiamo infatti che la variabile di interesse non sia elementare ma, ad esempio costituita da un insieme di dati. Potrebbe essere a questo punto eseguita una scrittura soltanto parziale, seguita da una lettura e quindi dal completamento della scrittura. Le operazioni verrebbero quindi eseguite con un insieme di valori modificato solo parzialmente, originando risultati privi di significato e dunque inaccettabili.
Considerata l'esistenza di questo genere di problematiche molti linguaggi di programmazione mettono a disposizione strumenti specifici per la gestione di variabili condivise. Esamineremo nel seguito in particolare come questa categoria di problemi viene affrontata dal linguaggio di programmazione Java, tra i più utilizzati in assoluto.
Innanzitutto è necessario comprendere come sia possibile garantire che soltanto un programma alla volta, o per essere più precisi un solo thread alla volta (un thread rappresenta la componente elementare di un processo o programma, in altre parole un programma può essere composto da più thread, eseguiti contemporaneamente) possa accedere ad una variabile condivisa. Il meccanismo che consente di offrire questa garanzia viene detto di mutua esclusione.
Mutua esclusione
Immaginiamo di sviluppare una classe (ovvero un insieme di dati e di metodi, cioè di funzioni utili a elaborare i dati stessi) di nome Variabile_Condivisa così strutturata:
public class Variabile_Condivisa { float euro_dollaro; float euro_sterlina; Variabile_Condivisa() { euro_dollaro = 1; euro_sterlina = 1; } void set_euro_dollaro(float e_d) {euro_dollaro=e_d;} void set_euro_sterlina(float e_s) {euro_sterlina=e_s;} float get_euro_dollaro() {return euro_dollaro;} float get_euro_sterlina() {return euro_sterlina;} }
I due dati, cioè euro_dollaro ed euro_sterlina, rappresentano i tassi di cambio euro/dollaro ed euro/sterlina e sono le informazioni che desideriamo condividere tra più programmi (o tra più thread). I metodi sviluppati consentono di assegnare un valore a tali dati (set_euro_dollaro e set_euro_sterlina) e di leggere tali valori (get_euro_dollaro e get_euro_sterlina).
Individuiamo poi un particolare metodo (Variabile_Condivisa ), che viene detto costruttore della classe e che viene eseguito alla creazione di ogni Variabile_Condivisa, impostando in questo caso i valori delle variabili euro_dollaro e euro_sterlina vengano impostati a 1.
Generiamo quindi all'interno del nostro programma un oggetto di tipo Variabile_Condivisa di nome var (la classe rappresenta un insieme di entità con caratteristiche comuni, mentre un oggetto rappresenta uno specifico elemento di tale insieme, a cui è possibile fare riferimento all'interno del programma) in questo modo:
Variabile_Condivisa var = new Variabile_Condivisa();
Come possiamo a questo punto garantire che non si verifichino problemi nella gestione del dato condiviso var? Il linguaggio Java mette a disposizione la parola-chiave (o keyword) synchronized, che accetta come parametro un qualsiasi oggetto. Tramite synchronized è possibile delimitare, come mostrato in esempio, dei blocchi di codice:
synchronized(var) { //blocco di codice delimitato da synchronized(var) }
Prima di eseguire le istruzioni contenute nel blocco synchronized, un qualsiasi thread acquisisce il lock sulla variabile var, ovvero blocca tutti gli ulteriori accessi al blocco di codice stesso fino a che tale lock non viene rilasciato, ovvero fino a dopo aver eseguito l'intero blocco di codice delimitato nell'esempio dalle parentesi graffe.
In altre parole, il primo thread (che immaginiamo di chiamare Primo) che esegue l'istruzione synchronized(var) crea di fatto una barriera che impedisce a qualunque altro thread di eseguire l'istruzione synchronized(var) fino a che Primo non ha completato l'esecuzione del blocco di codice delimitato da synchronized. In tali blocchi verranno dunque inserite le istruzioni di lettura o scrittura del dato condiviso.
In questo modo è garantita la mutua esclusione, ovvero si assicura che soltanto un thread alla volta possa accedere ad una variabile condivisa. Da notare però come sia necessario scegliere con cura l'oggetto da passare come parametro a synchronized. Deve trattarsi infatti di un oggetto comune a tutti i thread tra cui si desidera creare un meccanismo di mutua esclusione, ad esempio, come in questo caso, la variabile stessa che si desidera leggere o modificare.
Immaginiamo, a questo punto, di voler impostare il nostro programma in modo tale che i thread che desiderano leggere il valore della variabile var vengano messi in attesa e valutino il risultato soltanto dopo il primo aggiornamento successivo alla loro richiesta. Immaginiamo cioè di voler definire un meccanismo di sincronizzazione tra lettura e scrittura.
Sincronizzazione
La sincronizzazione risulta più complessa rispetto alla semplice mutua esclusione, visto che non soltanto definisce un'area di conflitto tra vari processi (cioè le variabili condivise), ma anche le politiche di gestione di tale conflitto. Nell'esempio, la lettura viene completata solo dopo una scrittura (si tratta cioè di un meccanismo detto di lettura bloccante).
Come possiamo realizzare la sincronizzazione in Java? Le istruzioni di riferimento saranno in questo caso: wait, notify e notifyAll. Anche in questo caso sarà necessario individuare un oggetto condiviso tra tutti i thread coinvolti, nel nostro caso ancora una volta var.
L'istruzione var.wait() verrà utilizzata per mettere in pausa un processo fino a che non riceverà un'opportuna segnalazione. Per segnalare a un processo in attesa su var che è possibile procedere utilizzeremo l'istruzione var.notify(). Se invece, come in questo caso, vogliamo rimettere in esecuzione tutti i processi in attesa sulla variabile var possiamo scegliere il comando var.notifyAll().
Considerando inoltre che la variabile var è condivisa, sarà necessario servirsi anche di synchronized per garantire, come visto in precedenza, anche la mutua esclusione, condizione necessaria per un corretto funzionamento e dunque anche per una corretta sincronizzazione.
In conclusione quindi il processo di scrittura potrà essere realizzato in questo modo (immaginando di assegnare a euro_dollaro il valore a e il valore b a euro_sterlina):
synchronized(var) { var.set_euro_dollaro(a); var.set_euro_dollaro(b); var.notifyAll(); }
La lettura (bloccante) sarà invece rappresentata da:
synchronized(var) { var.wait(); float euro_dollaro = var.get_euro_dollaro(); float euro_sterlina = var.get_euro_sterlina(); }