Sincronizarea thread-urilor - Operatorul synchronized
În activitatea thread-urilor, acestea apelează deseori la anumite resurse comune, care sunt definite în afara thread-ului, de exemplu, accesarea unui fișier. Dacă mai multe thread-uri accesează simultan o resursă comună, rezultatele execuției programului pot fi neașteptate și chiar imprevizibile. De exemplu, să definim următorul cod:
public class Program {
public static void main(String[] args) {
CommonResource commonResource = new CommonResource();
for (int i = 1; i < 6; i++) {
Thread t = new Thread(new CountThread(commonResource));
t.setName("Thread " + i);
t.start();
}
}
}
class CommonResource {
int x = 0;
}
class CountThread implements Runnable {
CommonResource res;
CountThread(CommonResource res) {
this.res = res;
}
public void run() {
res.x = 1;
for (int i = 1; i < 5; i++) {
System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x);
res.x++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
}
Aici este definită clasa CommonResource, care reprezintă o resursă comună și în care este definit un câmp întreg x.
Această resursă este utilizată de clasa thread-ului CountThread. Clasa crește pur și simplu în ciclu valoarea x cu 1. La intrarea în thread, valoarea x este setată la 1:
res.x = 1;
Prin urmare, ne așteptăm ca, după executarea ciclului, res.x să fie egal cu 4.
În clasa principală a programului, se lansează cinci thread-uri. Ne așteptăm ca fiecare thread să crească valoarea lui res.x de la 1 la 4, și asta de cinci ori. Cu toate acestea, rezultatul execuției programului va fi diferit:
Thread 1 1
Thread 2 1
Thread 3 1
Thread 5 1
Thread 4 1
Thread 5 6
Thread 2 6
Thread 1 6
Thread 3 6
Thread 4 6
Thread 4 11
Thread 2 11
Thread 5 11
Thread 3 11
Thread 1 11
Thread 4 16
Thread 1 16
Thread 3 16
Thread 5 16
Thread 2 16
Observăm că, înainte ca un thread să termine de lucrat cu câmpul res.x, un alt thread începe să lucreze cu el.
Pentru a evita această situație, trebuie să sincronizăm thread-urile. Una dintre metodele de sincronizare este utilizarea cuvântului cheie synchronized. Acest operator precede blocul de cod sau metoda care trebuie sincronizată. Pentru aplicare, vom modifica clasa CountThread:
class CountThread implements Runnable {
CommonResource res;
CountThread(CommonResource res) {
this.res = res;
}
public void run() {
synchronized (res) {
res.x = 1;
for (int i = 1; i < 5; i++) {
System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x);
res.x++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
}
}
Atunci când creăm un bloc de cod sincronizat, după operatorul synchronized urmează un obiect: synchronized(res). Acest obiect poate fi doar un obiect de clasă, nu un tip primitiv.
Fiecare obiect în Java are asociat un monitor. Monitorul reprezintă un fel de instrument pentru gestionarea accesului la obiect. Când execuția codului ajunge la operatorul synchronized, monitorul obiectului res este blocat, iar pe durata blocării sale, accesul la blocul de cod este monopolizat de un singur thread, cel care a realizat blocarea.
După finalizarea blocului de cod, monitorul obiectului res este eliberat și devine disponibil pentru alte thread-uri.
După eliberarea monitorului, un alt thread îl poate prelua, iar restul thread-urilor continuă să aștepte eliberarea lui.
Rezultatul în consolă va fi acum diferit:
Thread 1 1
Thread 1 2
Thread 1 3
Thread 1 4
Thread 3 1
Thread 3 2
Thread 3 3
Thread 3 4
Thread 5 1
Thread 5 2
Thread 5 3
Thread 5 4
Thread 4 1
Thread 4 2
Thread 4 3
Thread 4 4
Thread 2 1
Thread 2 2
Thread 2 3
Thread 2 4
Când se aplică operatorul synchronized la o metodă, până când metoda nu își finalizează execuția, accesul este monopolizat doar de un singur thread – primul care a început să o execute. Pentru aplicarea synchronized la o metodă, modificăm clasele din program:
public class Program {
public static void main(String[] args) {
CommonResource commonResource = new CommonResource();
for (int i = 1; i < 6; i++) {
Thread t = new Thread(new CountThread(commonResource));
t.setName("Thread " + i);
t.start();
}
}
}
class CommonResource {
int x;
synchronized void increment() {
x = 1;
for (int i = 1; i < 5; i++) {
System.out.printf("%s %d \n", Thread.currentThread().getName(), x);
x++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
}
class CountThread implements Runnable {
CommonResource res;
CountThread(CommonResource res) {
this.res = res;
}
public void run() {
res.increment();
}
}
Rezultatul în acest caz va fi similar cu cel din exemplul anterior, cu blocul synchronized. Din nou, monitorul obiectului CommonResource intră în joc – acesta este obiectul comun pentru toate thread-urile.
De aceea, nu metoda run() din clasa CountThread este sincronizată, ci metoda increment din clasa CommonResource. Când primul thread începe să execute metoda increment, el capturează monitorul obiectului CommonResource. Iar restul thread-urilor continuă să aștepte eliberarea acestuia.