"Bună, Amigo! Îți amintești că Ellie ți-a spus despre problemele care apar atunci când mai multe fire încearcă să acceseze simultan o resursă partajată, da?"

"Da."

"Chestia este că asta nu e tot. Mai este o mică problemă."

După cum știți, un computer are memorie în care sunt stocate datele și comenzile (codul), precum și un procesor care execută aceste comenzi și lucrează cu datele. Procesorul citește datele din memorie, le modifică și le scrie înapoi în memorie. Pentru a accelera calculele, procesorul are propria sa memorie „rapidă” încorporată: cache-ul.

Procesorul rulează mai rapid prin copierea variabilelor și zonelor de memorie cele mai frecvent utilizate în memoria cache. Apoi face toate modificările în această memorie rapidă. Și apoi copiază datele înapoi în memoria „lentă”. În tot acest timp, memoria lentă conține variabilele vechi (neschimbate!).

Aici apare problema. Un fir de execuție modifică o variabilă , cum ar fi isCancel sau isInterrupted în exemplul de mai sus, dar un al doilea thread «nu vede această schimbare , deoarece s-a întâmplat în memoria rapidă. Aceasta este o consecință a faptului că firele de execuție nu au acces unul la memoria cache a celuilalt. (Un procesor conține adesea mai multe nuclee independente, iar firele de execuție pot rula pe nuclee diferite din punct de vedere fizic.)

Să ne amintim exemplul de ieri:

Cod Descriere
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");
}
}
}
Firul „nu știe” că celelalte fire există.

În metoda run, variabila isCancel este introdusă în memoria cache a firului de execuție copil atunci când este utilizată pentru prima dată. Această operație este echivalentă cu următorul cod:

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

Apelarea metodei cancel dintr-un alt thread va schimba valoarea isCancel în memoria normală (lentă), dar nu și în cache-urile altor fire.

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

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

"Woa! Și au venit cu o soluție frumoasă și pentru asta, cum ar fi  sincronizarea ?"

— N-o să crezi!

Primul gând a fost să dezactivezi memoria cache, dar asta a făcut ca programele să ruleze de câteva ori mai încet. Apoi a apărut o soluție diferită.

S-a născut cuvântul cheie volatil . Am pus acest cuvânt cheie înaintea unei declarații de variabilă pentru a indica faptul că valoarea sa nu trebuie pusă în cache. Mai precis, nu a fost că nu ar putea fi introdus în cache, ci pur și simplu a trebuit să fie întotdeauna citit și scris în memoria normală (lentă).

Iată cum să remediați soluția noastră, astfel încât totul să funcționeze bine:

Cod Descriere
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");
}
}
}
Modificatorul volatil face ca o variabilă să fie întotdeauna citită și scrisă în memoria normală partajată de toate firele de execuție.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

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

"Asta este?"

"Asta e. Simplu și frumos."