Bună! Astăzi vom continua să luăm în considerare caracteristicile programării cu mai multe fire și să vorbim despre sincronizarea firelor.
Ce este sincronizarea în Java?
În afara domeniului de programare, implică un aranjament care permite a două dispozitive sau programe să lucreze împreună. De exemplu, un smartphone și un computer pot fi sincronizate cu un cont Google, iar un cont de site web poate fi sincronizat cu conturile de rețele sociale, astfel încât să le puteți utiliza pentru a vă conecta. reciproc. În lecțiile anterioare, firele noastre au trăit și au lucrat separat unele de altele. Unul a făcut un calcul, al doilea a dormit, iar al treilea a afișat ceva pe consolă, dar nu au interacționat. În programele reale, astfel de situații sunt rare. Firele multiple pot lucra activ și pot modifica același set de date. Acest lucru creează probleme. Imaginează-ți mai multe fire care scriu text în același loc, de exemplu, într-un fișier text sau pe consolă. În acest caz, fișierul sau consola devine o resursă partajată. Firele nu sunt conștiente de existența celuilalt, așa că pur și simplu scriu tot ce pot în timpul alocat lor de către planificatorul de fire. Într-o lecție recentă, am văzut un exemplu despre unde duce acest lucru. Să ne amintim acum: Motivul constă în faptul că firele de execuție lucrează cu o resursă partajată (consola) fără a-și coordona acțiunile între ele. Dacă planificatorul de fire alocă timp Thread-1, atunci scrie instantaneu totul în consolă. Ce alte fire au sau nu au reușit deja să scrie nu contează. Rezultatul, după cum puteți vedea, este deprimant. De aceea au introdus un concept special, mutex (excluderea reciprocă) , în programarea multithreaded. Scopul unui mutexeste de a oferi un mecanism astfel încât doar un fir să aibă acces la un obiect la un anumit moment. Dacă Thread-1 dobândește mutex-ul obiectului A, celelalte fire nu vor putea accesa și modifica obiectul. Celelalte fire trebuie să aștepte până când mutexul obiectului A este eliberat. Iată un exemplu din viață: imaginează-ți că tu și alți 10 străini participați la un exercițiu. Pe rând, trebuie să vă exprimați ideile și să discutați ceva. Dar pentru că vă vedeți pentru prima dată, pentru a nu vă întrerupe constant și a zbura în furie, folosiți o „minge care vorbește”: doar persoana cu mingea poate vorbi. Astfel ajungi să ai o discuție bună și fructuoasă. În esență, mingea este un mutex. Dacă mutexul unui obiect este în mâinile unui fir, alte fire nu pot funcționa cu obiectul.Object
clasa, ceea ce înseamnă că fiecare obiect din Java are unul.
Cum funcționează operatorul sincronizat
Să facem cunoștință cu un nou cuvânt cheie: sincronizat . Este folosit pentru a marca un anumit bloc de cod. Dacă un bloc de cod este marcat cusynchronized
cuvântul cheie, atunci acel bloc poate fi executat doar de un fir la un moment dat. Sincronizarea poate fi implementată în diferite moduri. De exemplu, prin declararea unei întregi metode care urmează să fie sincronizată:
public synchronized void doSomething() {
// ...Method logic
}
Sau scrieți un bloc de cod în care sincronizarea este efectuată folosind un obiect:
public class Main {
private Object obj = new Object();
public void doSomething() {
// ...Some logic available simultaneously to all threads
synchronized (obj) {
// Logic available to just one thread at a time
}
}
}
Sensul este simplu. Dacă un fir de execuție intră în blocul de cod marcat cu synchronized
cuvântul cheie, captează instantaneu mutex-ul obiectului, iar toate celelalte fire care încearcă să intre în același bloc sau metodă sunt forțate să aștepte până când firul anterior își finalizează activitatea și eliberează monitorul. Apropo! În timpul cursului, ați văzut deja exemple de synchronized
, dar arătau diferit:
public void swap()
{
synchronized (this)
{
// ...Method logic
}
}
Subiectul este nou pentru tine. Și, desigur, va exista confuzie cu sintaxa. Așadar, memorează-l imediat pentru a nu fi confundat mai târziu de diferitele moduri de a-l scrie. Aceste două moduri de a-l scrie înseamnă același lucru:
public void swap() {
synchronized (this)
{
// ...Method logic
}
}
public synchronized void swap() {
}
}
În primul caz, creați un bloc de cod sincronizat imediat după introducerea metodei. Este sincronizat de this
obiect, adică de obiectul curent. Și în al doilea exemplu, aplicați synchronized
cuvântul cheie întregii metode. Acest lucru face să nu fie necesară indicarea explicită a obiectului utilizat pentru sincronizare. Deoarece întreaga metodă este marcată cu cuvântul cheie, metoda va fi sincronizată automat pentru toate instanțele clasei. Nu ne vom scufunda într-o discuție despre ce cale este mai bună. Deocamdată, alegeți calea care vă place cel mai mult :) Principalul lucru este să rețineți: puteți declara o metodă sincronizată numai atunci când toată logica sa este executată de un fir la un moment dat. De exemplu, ar fi o greșeală să sincronizați următoarea doSomething()
metodă:
public class Main {
private Object obj = new Object();
public void doSomething() {
// ...Some logic available simultaneously to all threads
synchronized (obj) {
// Logic available to just one thread at a time
}
}
}
După cum puteți vedea, o parte a metodei conține o logică care nu necesită sincronizare. Acest cod poate fi rulat de mai multe fire în același timp și toate locurile critice sunt puse deoparte într-un synchronized
bloc separat. Și încă ceva. Să examinăm îndeaproape exemplul nostru din lecția cu schimbarea numelor:
public void swap()
{
synchronized (this)
{
// ...Method logic
}
}
Notă: sincronizarea se realizează folosindthis
. Adică folosirea unuiMyClass
obiect anume. Să presupunem că avem 2 fire (Thread-1
șiThread-2
) și un singurMyClass myClass
obiect. În acest caz, dacăThread-1
apeleazămyClass.swap()
metoda, mutex-ul obiectului va fi ocupat, iar când se încearcă apelarea, metodamyClass.swap()
seThread-2
va bloca în timp ce se așteaptă eliberarea mutex-ului. Dacă vom avea 2 fire și 2MyClass
obiecte (myClass1
șimyClass2
), firele noastre pot executa cu ușurință simultan metodele sincronizate pe diferite obiecte. Primul thread execută asta:
myClass1.swap();
Al doilea execută asta:
myClass2.swap();
În acest caz, synchronized
cuvântul cheie din cadrul swap()
metodei nu va afecta funcționarea programului, deoarece sincronizarea se realizează folosind un anumit obiect. Și în acest din urmă caz, avem 2 obiecte. Astfel, firele nu își creează probleme unul altuia. La urma urmei, două obiecte au 2 mutexuri diferite, iar obținerea unuia este independentă de achiziționarea celuilalt .
Caracteristici speciale ale sincronizării în metode statice
Dar ce se întâmplă dacă trebuie să sincronizați o metodă statică ?
class MyClass {
private static String name1 = "Ally";
private static String name2 = "Lena";
public static synchronized void swap() {
String s = name1;
name1 = name2;
name2 = s;
}
}
Nu este clar ce rol va juca mutexul aici. La urma urmei, am stabilit deja că fiecare obiect are un mutex. Dar problema este că nu avem nevoie de obiecte pentru a apela MyClass.swap()
metoda: metoda este statică! Deci ce urmeaza? :/ De fapt, nu este nicio problemă aici. Creatorii Java s-au ocupat de tot :) Dacă o metodă care conține o logică concurență critică este statică, atunci sincronizarea este efectuată la nivel de clasă. Pentru o mai mare claritate, putem rescrie codul de mai sus după cum urmează:
class MyClass {
private static String name1 = "Ally";
private static String name2 = "Lena";
public static void swap() {
synchronized (MyClass.class) {
String s = name1;
name1 = name2;
name2 = s;
}
}
}
În principiu, te-ai fi putut gândi la asta: Deoarece nu există obiecte, mecanismul de sincronizare trebuie cumva să fie integrat în clasa însăși. Și așa este: putem folosi clase pentru a sincroniza.
GO TO FULL VERSION