CodeGym/Curs Java/Modulul 3/Memoria în JVM, partea 2

Memoria în JVM, partea 2

Disponibil

Arhitectura hardware de memorie

Arhitectura hardware de memorie modernă diferă de modelul de memorie internă Java. Prin urmare, trebuie să înțelegeți arhitectura hardware pentru a ști cum funcționează modelul Java cu aceasta. Această secțiune descrie arhitectura hardware generală a memoriei, iar secțiunea următoare descrie modul în care funcționează Java cu aceasta.

Iată o diagramă simplificată a arhitecturii hardware a unui computer modern:

Arhitectura hardware de memorie

În lumea modernă, un computer are 2 sau mai multe procesoare și aceasta este deja norma. Unele dintre aceste procesoare pot avea, de asemenea, mai multe nuclee. Pe astfel de computere, este posibil să rulați mai multe fire în același timp. Fiecare nucleu de procesor este capabil să execute un fir la un moment dat. Aceasta înseamnă că orice aplicație Java este a priori multi-threaded, iar în cadrul programului dvs., un fir de execuție per nucleu de procesor poate rula odată.

Miezul procesorului conține un set de registre care se află în memoria sa (în interiorul nucleului). Efectuează operațiuni cu datele din registru mult mai rapid decât pe datele care se află în memoria principală (RAM) a computerului. Acest lucru se datorează faptului că procesorul poate accesa aceste registre mult mai rapid.

Fiecare CPU poate avea, de asemenea, propriul strat de cache. Majoritatea procesoarelor moderne o au. Procesorul își poate accesa memoria cache mult mai rapid decât memoria principală, dar nu la fel de rapid ca registrele sale interne. Valoarea vitezei de acces la cache este aproximativ între vitezele de acces ale memoriei principale și ale registrelor interne.

Mai mult, procesoarele au un loc pentru a avea un cache pe mai multe niveluri. Dar acest lucru nu este atât de important de știut pentru a înțelege cum interacționează modelul de memorie Java cu memoria hardware. Este important de știut că procesoarele pot avea un anumit nivel de cache.

Orice computer conține și RAM (zona de memorie principală) în același mod. Toate nucleele pot accesa memoria principală. Zona de memorie principală este de obicei mult mai mare decât memoria cache a nucleelor ​​procesorului.

În momentul în care procesorul are nevoie de acces la memoria principală, citește o parte din aceasta în memoria sa cache. De asemenea, poate citi unele date din cache în registrele sale interne și apoi poate efectua operațiuni asupra acestora. Când CPU trebuie să scrie rezultatul înapoi în memoria principală, va șterge datele din registrul său intern în cache și, la un moment dat, în memoria principală.

Datele stocate în cache sunt în mod normal șterse înapoi în memoria principală atunci când procesorul trebuie să stocheze altceva în cache. Cache-ul are capacitatea de a-și șterge memoria și de a scrie date în același timp. Procesorul nu trebuie să citească sau să scrie memoria cache completă de fiecare dată în timpul unei actualizări. De obicei, memoria cache este actualizată în blocuri mici de memorie, acestea fiind numite „linie cache”. Una sau mai multe „linii de cache” pot fi citite în memoria cache, iar una sau mai multe linii de cache pot fi scoase înapoi în memoria principală.

Combinând modelul de memorie Java și arhitectura hardware de memorie

După cum sa menționat deja, modelul de memorie Java și arhitectura hardware de memorie sunt diferite. Arhitectura hardware nu face distincție între stivele de fire și grămezi. Pe hardware, stiva de fire și HEAP (heap) se află în memoria principală.

Părți de stive și grămezi de fire pot fi uneori prezente în cache-urile și registrele interne ale CPU. Acest lucru este prezentat în diagramă:

stiva de fire și HEAP

Atunci când obiectele și variabilele pot fi stocate în diferite zone ale memoriei computerului, pot apărea anumite probleme. Iată cele două principale:

  • Vizibilitatea modificărilor pe care thread-ul le-a făcut variabilelor partajate.
  • Condiția de cursă la citirea, verificarea și scrierea variabilelor partajate.

Ambele probleme vor fi explicate mai jos.

Vizibilitatea obiectelor partajate

Dacă două sau mai multe fire de execuție partajează un obiect fără utilizarea adecvată a declarației volatile sau a sincronizării, atunci modificările aduse obiectului partajat făcute de un fir de execuție pot să nu fie vizibile pentru alte fire.

Imaginează-ți că un obiect partajat este stocat inițial în memoria principală. Un thread care rulează pe un CPU citește obiectul partajat în memoria cache a aceluiași CPU. Acolo face modificări obiectului. Până când memoria cache a procesorului a fost golită în memoria principală, versiunea modificată a obiectului partajat nu este vizibilă pentru firele care rulează pe alte procesoare. Astfel, fiecare thread poate obține propria copie a obiectului partajat, fiecare copie va fi într-un cache separat al CPU.

Următoarea diagramă ilustrează o schiță a acestei situații. Un fir de execuție care rulează pe CPU din stânga copiază obiectul partajat în cache-ul său și schimbă valoarea numărului la 2. Această modificare este invizibilă pentru alte fire de execuție care rulează pe CPU-ul din dreapta, deoarece actualizarea de numărare nu a fost încă șters înapoi în memoria principală.

Pentru a rezolva această problemă, puteți folosi cuvântul cheie volatil atunci când declarați o variabilă. Se poate asigura că o anumită variabilă este citită direct din memoria principală și este întotdeauna scrisă înapoi în memoria principală atunci când este actualizată.

Stare de cursă

Dacă două sau mai multe fire de execuție partajează același obiect și mai multe fire de execuție actualizează variabile în acel obiect partajat, atunci poate apărea o condiție de concurență.

Imaginați-vă că firul A citește variabila de numărare a obiectului partajat în memoria cache a procesorului său. Imaginați-vă, de asemenea, că firul B face același lucru, dar în memoria cache a altui procesor. Acum firul A adaugă 1 la valoarea numărului, iar firul B face același lucru. Acum variabila a fost mărită de două ori - separat cu +1 în memoria cache a fiecărui procesor.

Dacă aceste incremente ar fi efectuate secvențial, variabila de numărare ar fi dublată și scrisă înapoi în memoria principală (valoarea inițială + 2).

Cu toate acestea, două creșteri au fost efectuate în același timp fără o sincronizare adecvată. Indiferent de firul de execuție (A sau B) care își scrie versiunea actualizată de numărare în memoria principală, noua valoare va fi doar cu 1 mai mult decât valoarea inițială, în ciuda celor două incremente.

Această diagramă ilustrează apariția problemei condiției de cursă descrisă mai sus:

Pentru a rezolva această problemă, puteți utiliza blocul sincronizat Java. Un bloc sincronizat asigură că doar un fir poate intra într-o anumită secțiune critică de cod la un moment dat.

Blocurile sincronizate garantează, de asemenea, că toate variabilele accesate în interiorul blocului sincronizat vor fi citite din memoria principală, iar când firul de execuție iese din blocul sincronizat, toate variabilele actualizate vor fi șters înapoi în memoria principală, indiferent dacă variabila este declarată volatilă sau Nu.

Comentarii
  • Popular
  • Nou
  • Vechi
Trebuie să fii conectat pentru a lăsa un comentariu
Această pagină nu are încă niciun comentariu