"Ciao, Amigo! Ti ricordi che Ellie ti ha parlato dei problemi che sorgono quando diversi thread tentano di accedere contemporaneamente a una risorsa condivisa, vero?"

"SÌ."

"Il fatto è che non è tutto. C'è un altro piccolo problema."

Come sapete, un computer ha una memoria in cui sono memorizzati dati e comandi (codice), nonché un processore che esegue questi comandi e lavora con i dati. Il processore legge i dati dalla memoria, li modifica e li riscrive in memoria. Per velocizzare i calcoli, il processore ha una propria memoria "veloce" incorporata: la cache.

Il processore funziona più velocemente copiando le variabili e le aree di memoria utilizzate più di frequente nella sua cache. Quindi apporta tutte le modifiche in questa memoria veloce. E poi copia i dati nella memoria «lenta». Per tutto questo tempo, la memoria lenta contiene le vecchie variabili (immutate!).

È qui che sorge il problema. Un thread modifica una variabile , come isCancel o isInterrupted nell'esempio sopra, ma un secondo thread «non vede questo cambiamento , perché è avvenuto nella memoria veloce. Questa è una conseguenza del fatto che i thread non hanno accesso alla cache dell'altro. (Un processore spesso contiene diversi core indipendenti e i thread possono essere eseguiti su core fisicamente diversi.)

Ricordiamo l'esempio di ieri:

Codice Descrizione
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
Il thread «non sa» che esistono gli altri thread.

Nel metodo run, la variabile isCancel viene inserita nella cache del thread figlio quando viene utilizzata per la prima volta. Questa operazione è equivalente al seguente codice:

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}

Chiamare il metodo cancel da un altro thread cambierà il valore di isCancel nella memoria normale (lenta), ma non nelle cache di altri thread.

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

"Whoa! E hanno trovato una bella soluzione anche per questo, come con  la sincronizzazione ?"

"Non ci crederai!"

Il primo pensiero è stato quello di disabilitare la cache, ma questo ha reso i programmi più lenti. Poi è emersa una soluzione diversa.

La parola chiave volatile è nata. Mettiamo questa parola chiave prima di una dichiarazione di variabile per indicare che il suo valore non deve essere messo nella cache. Più precisamente, non era che non potesse essere inserito nella cache, era semplicemente che doveva sempre essere letto e scritto nella normale (lenta) memoria.

Ecco come correggere la nostra soluzione in modo che tutto funzioni correttamente:

Codice Descrizione
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
Il modificatore volatile fa sì che una variabile venga sempre letta e scritta nella memoria normale condivisa da tutti i thread.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

"Questo è tutto?"

"Esatto. Semplice e bello."