Bună! Continuăm studiul nostru despre multithreading. Astăzi vom cunoaște cuvântul
Când numim
volatile
cheie și yield()
metoda. Hai sa ne scufundam :)
Cuvântul cheie volatil
Când creăm aplicații multithreaded, ne putem confrunta cu două probleme serioase. În primul rând, atunci când rulează o aplicație cu mai multe fire de execuție, diferite fire de execuție pot stoca în cache valorile variabilelor (am vorbit deja despre asta în lecția intitulată „Utilizarea volatilelor” ). Puteți avea situația în care un fir de execuție modifică valoarea unei variabile, dar un al doilea thread nu vede modificarea, deoarece funcționează cu copia sa în cache a variabilei. Desigur, consecințele pot fi grave. Să presupunem că nu este vorba despre orice variabilă veche, ci mai degrabă de soldul contului dvs. bancar, care brusc începe să sară aleatoriu în sus și în jos :) Asta nu sună a distractiv, nu? În al doilea rând, în Java, operațiuni de citire și scriere a tuturor tipurilor primitive,long
double
, sunt atomice. Ei bine, de exemplu, dacă modificați valoarea unei int
variabile pe un fir, iar pe alt fir citiți valoarea variabilei, veți obține fie valoarea ei veche, fie cea nouă, adică valoarea care a rezultat din modificare în firul 1. Nu există „valori intermediare”. Totuși, acest lucru nu funcționează cu long
s și double
s. De ce? Datorită suportului multiplatform. Vă amintiți la nivelurile de început că am spus că principiul călăuzitor al Java este „scrieți o dată, rulați oriunde”? Asta înseamnă suport multiplatform. Cu alte cuvinte, o aplicație Java rulează pe tot felul de platforme diferite. De exemplu, pe sistemele de operare Windows, diferite versiuni de Linux sau MacOS. Va rula fără probleme pe toate. Cântărind 64 de biți,long
double
sunt cele mai „grele” primitive din Java. Și anumite platforme pe 32 de biți pur și simplu nu implementează citirea și scrierea atomică a variabilelor pe 64 de biți. Astfel de variabile sunt citite și scrise în două operații. Mai întâi, primii 32 de biți sunt scrieți în variabilă, apoi alți 32 de biți sunt scrieți. Ca urmare, poate apărea o problemă. Un fir de execuție scrie o valoare de 64 de biți într-o X
variabilă și face acest lucru în două operații. În același timp, un al doilea thread încearcă să citească valoarea variabilei și face acest lucru între cele două operații - atunci când primii 32 de biți au fost scrisi, dar al doilea 32 de biți nu. Ca rezultat, citește o valoare intermediară, incorectă și avem o eroare. De exemplu, dacă pe o astfel de platformă încercăm să scriem numărul într-un 9223372036854775809 la o variabilă, aceasta va ocupa 64 de biți. În formă binară, arată astfel: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 Primul fir începe să scrie numărul în variabilă. La început, scrie primii 32 de biți (1000000000000000000000000000000) și apoi pe al doilea 32 de biți (0000000000000000000000000000001) Iar cel de-al doilea thread se poate bloca între aceste operații, citind valoarea intermediară a variabilei (1000000000000000000000000000000), care sunt primii 32 de biți care au fost deja scriși. În sistemul zecimal, acest număr este 2.147.483.648. Cu alte cuvinte, am vrut doar să scriem numărul 9223372036854775809 într-o variabilă, dar datorită faptului că această operațiune nu este atomică pe unele platforme, avem numărul malefic 2.147.483.648, care a apărut de nicăieri și va avea un efect necunoscut program. Cel de-al doilea thread a citit pur și simplu valoarea variabilei înainte ca aceasta să fi terminat de scris, adică firul a văzut primii 32 de biți, dar nu și cei de-a doua 32 de biți. Desigur, aceste probleme nu au apărut ieri. Java le rezolvă cu un singur cuvânt cheie: volatile
. Dacă folosimvolatile
cuvânt cheie atunci când declarăm o variabilă în programul nostru...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…înseamnă că:
- Va fi întotdeauna citit și scris atomic. Chiar dacă este pe 64 de biți
double
saulong
. - Mașina Java nu o va stoca în cache. Deci nu veți avea o situație în care 10 fire să lucreze cu propriile copii locale.
Metoda yield().
Am trecut deja în revistă multe dintreThread
metodele clasei, dar există una importantă care va fi nouă pentru tine. Este yield()
metoda . Și face exact ceea ce sugerează numele său! ![Gestionarea thread-urilor. Cuvântul cheie volatil și metoda yield() - 2](https://cdn.codegym.cc/images/article/a54c61ab-824d-477c-bc12-b00adbc12082/800.jpeg)
yield
metoda pe un thread, ea vorbește de fapt cu celelalte fire: „Hei, băieți. Nu mă grăbesc în mod deosebit să merg nicăieri, așa că, dacă este important ca vreunul dintre voi să obțină timp de procesor, ia-l – pot să aștept”. Iată un exemplu simplu despre cum funcționează:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
Creăm și începem secvenţial trei fire: Thread-0
, Thread-1
, și Thread-2
. Thread-0
începe primul și cedează imediat celorlalți. Apoi Thread-1
este pornit și, de asemenea, cedează. Apoi Thread-2
se începe, care cedează și el. Nu mai avem fire de execuție și, după ce Thread-2
a cedat ultimul loc, programatorul de fire spune: „Hmm, nu mai există fire noi. Pe cine avem la coada? Cine și-a cedat locul înainte Thread-2
? Se pare că a fost Thread-1
. Bine, asta înseamnă că o vom lăsa să curgă'. Thread-1
își încheie munca și apoi planificatorul de fire își continuă coordonarea: „Bine, Thread-1
terminat. Mai avem pe cineva la coadă?'. Thread-0 este în coadă: și-a cedat locul chiar înainteThread-1
. Acum îi vine rândul și merge până la final. Apoi programatorul termină de coordonat firele: „Bine, Thread-2
, ai cedat altor fire și toate s-au terminat acum. Ai fost ultimul care a cedat, așa că acum e rândul tău'. Apoi Thread-2
rulează până la finalizare. Ieșirea consolei va arăta astfel: Thread-0 își cedează locul altora Thread-1 cede locul său altora Thread-2 cede locul său altora Thread-1 a terminat de executat. Thread-0 s-a terminat de execuție. Thread-2 s-a terminat de execuție. Desigur, programatorul de fire ar putea începe firele într-o ordine diferită (de exemplu, 2-1-0 în loc de 0-1-2), dar principiul rămâne același.
Se întâmplă înainte de reguli
Ultimul lucru pe care îl vom atinge astăzi este conceptul de „ se întâmplă înainte ”. După cum știți deja, în Java, programatorul de fire realizează cea mai mare parte a muncii implicate în alocarea timpului și a resurselor firelor pentru a-și îndeplini sarcinile. De asemenea, ați văzut în mod repetat cum firele de execuție sunt executate într-o ordine aleatorie care este de obicei imposibil de prezis. Și, în general, după programarea „secvențială” pe care am făcut-o anterior, programarea cu mai multe fire arată ca ceva aleatoriu. Ați ajuns deja să credeți că puteți utiliza o serie de metode pentru a controla fluxul unui program multithread. Dar multithreadingul în Java are încă un pilon – regulile 4 „ se întâmplă-înainte ”. Înțelegerea acestor reguli este destul de simplă. Imaginați-vă că avem două fire -A
șiB
. Fiecare dintre aceste fire poate efectua operații 1
și 2
. În fiecare regulă, când spunem „ A se întâmplă-înainte de B ”, ne referim la faptul că toate modificările făcute de thread A
înainte de operare 1
și modificările rezultate din această operație sunt vizibile pentru thread B
atunci când operația 2
este efectuată și după aceea. Fiecare regulă garantează că atunci când scrieți un program cu mai multe fire, anumite evenimente vor avea loc înaintea altora 100% din timp și că în momentul operațiunii 2
firul B
va fi întotdeauna la curent cu modificările pe care le- A
a făcut în timpul funcționării 1
. Să le revizuim.
Regula 1.
Eliberarea unui mutex are loc înainte ca același monitor să fie achiziționat de un alt fir. Cred că înțelegi totul aici. Dacă mutexul unui obiect sau al unei clase este dobândit de un fir de execuție, de exemplu, de un fir de execuție ,A
un alt fir de execuție (fir de execuție B
) nu îl poate achiziționa în același timp. Trebuie să aștepte până când mutexul este eliberat.
Regula 2.
MetodaThread.start()
se întâmplă înainte Thread.run()
. Din nou, nimic greu aici. Știți deja că pentru a începe să rulați codul în interiorul run()
metodei, trebuie să apelați start()
metoda pe fir. Mai exact, metoda de pornire, nu run()
metoda în sine! Această regulă asigură că valorile tuturor variabilelor setate înainte Thread.start()
de apelare vor fi vizibile în interiorul run()
metodei odată ce începe.
Regula 3.
Sfârșitul metodeirun()
are loc înainte de întoarcerea de la join()
metodă. Să revenim la cele două fire ale noastre: A
și B
. Apelăm join()
metoda astfel încât firul B
să aştepte finalizarea firului A
înainte de a-şi face treaba. Aceasta înseamnă că metoda obiectului A run()
este garantată să ruleze până la capăt. Și toate modificările aduse datelor care au loc în run()
metoda thread-ului A
sunt garantate sută la sută a fi vizibile în thread B
odată ce a terminat, așteptând ca firul A
să-și termine activitatea, astfel încât să își poată începe propria lucrare.
Regula 4.
Scrierea într-ovolatile
variabilă are loc înainte de a citi din aceeași variabilă. Când folosim volatile
cuvântul cheie, primim întotdeauna valoarea curentă. Chiar și cu un long
sau double
(am vorbit mai devreme despre problemele care se pot întâmpla aici). După cum înțelegeți deja, modificările efectuate pe unele fire nu sunt întotdeauna vizibile pentru alte fire. Dar, desigur, sunt situații foarte frecvente în care un astfel de comportament nu ni se potrivește. Să presupunem că atribuim o valoare unei variabile de pe fir A
:
int z;
….
z = 555;
Dacă B
firul nostru ar trebui să afișeze valoarea variabilei z
pe consolă, ar putea afișa cu ușurință 0, deoarece nu știe despre valoarea atribuită. Dar Regula 4 garantează că, dacă declarăm variabila z
ca volatile
, atunci modificările valorii sale pe un fir vor fi întotdeauna vizibile pe alt fir. Dacă adăugăm cuvântul volatile
la codul anterior...
volatile int z;
….
z = 555;
...apoi prevenim situația în care firul B
ar putea afișa 0. Scrierea în volatile
variabile are loc înainte de a citi din ele.
GO TO FULL VERSION