Multithreading în Java

Mașina virtuală Java acceptă calculul paralel . Toate calculele pot fi efectuate în contextul unuia sau mai multor fire. Putem configura cu ușurință accesul la aceeași resursă sau obiect pentru mai multe fire de execuție, precum și configurarea unui fir pentru a executa un singur bloc de cod.

Orice dezvoltator trebuie să sincronizeze lucrul cu firele în timpul operațiunilor de citire și scriere pentru resursele care au mai multe fire de execuție alocate.

Este important ca in momentul accesarii resursei sa ai date la zi pentru ca un alt thread sa o poata schimba si sa obtii cele mai actualizate informatii. Chiar dacă luăm exemplul unui cont bancar, până nu ajung banii în el, nu îl poți folosi, așa că este important să ai mereu date la zi. Java are clase speciale pentru sincronizarea și gestionarea thread-urilor.

Obiecte de fir

Totul începe cu firul principal (principal), adică cel puțin programul dvs. are deja un fir de execuție. Firul principal poate crea alte fire folosind Callable sau Runnable . Crearea diferă doar în rezultatul returnat, Runnable nu returnează un rezultat și nu poate arunca o excepție bifată. Prin urmare, aveți o bună oportunitate de a construi o muncă eficientă cu fișiere, dar acest lucru este foarte periculos și trebuie să fiți atenți.

De asemenea, este posibil să programați execuția firului de execuție pe un nucleu separat al procesorului. Sistemul se poate deplasa cu ușurință între fire și executa un anumit thread cu setările potrivite: adică primul thread care citește datele este executat, de îndată ce avem date, apoi îl trecem firului care este responsabil de validare, după aceea îl trecem firului de execuție pentru a executa o logică de afaceri și apoi un nou thread le scrie înapoi. Într-o astfel de situație, 4 fire de execuție procesează datele pe rând și totul va funcționa mai repede decât un fir. Fiecare astfel de flux este convertit într-un flux de sistem de operare nativ, dar modul în care va fi convertit depinde de implementarea JVM.

Clasa Thread este folosită pentru a crea și lucra cu fire. Are mecanisme de control standard, precum și abstracte, cum ar fi clase și colecții din java.util.concurrent .

Sincronizarea firelor în Java

Comunicarea este asigurată prin partajarea accesului la obiecte. Acest lucru este foarte eficient, dar în același timp este foarte ușor să faceți o greșeală când lucrați. Erorile apar în două cazuri: interferența firului - când un alt fir interferează cu firul dvs. și erori de consistență a memoriei - consistența memoriei. Pentru a rezolva și a preveni aceste erori, avem diferite metode de sincronizare.

Sincronizarea firelor în Java este gestionată de monitoare, acesta este un mecanism de nivel înalt care permite doar unui fir să execute un bloc de cod protejat de același monitor la un moment dat. Comportamentul monitoarelor este considerat în termeni de încuietori; un monitor - o lacăt.

Sincronizarea are câteva puncte importante cărora trebuie să le acordați atenție. Primul punct este excluderea reciprocă - doar un fir de execuție poate deține monitorul, astfel sincronizarea pe monitor implică că odată ce un fir de execuție intră într-un bloc sincronizat protejat de monitor, niciun alt fir de execuție nu poate intra în blocul protejat de monitor. primul fir iese din blocul sincronizat. Adică, mai multe fire nu pot accesa același bloc sincronizat în același timp.

Dar sincronizarea nu este doar excludere reciprocă. Sincronizarea asigură că datele scrise în memorie înainte sau în cadrul unui bloc sincronizat devin vizibile pentru alte fire care sunt sincronizate pe același monitor. După ce ieșim din bloc, eliberăm monitorul și un alt thread îl poate apuca și începe să execute acest bloc de cod.

Când un nou thread captează monitorul, obținem acces și capacitatea de a executa acel bloc de cod, iar în acel moment variabilele vor fi încărcate din memoria principală. Apoi putem vedea toate intrările făcute vizibile de ediția anterioară a monitorului.

O citire-scriere pe un câmp este o operație atomică dacă câmpul este fie declarat volatil , fie protejat de o blocare unică dobândită înainte de orice citire-scriere. Dar dacă tot întâmpinați o eroare, atunci veți primi o eroare despre reordonare (schimbarea ordinii, reordonarea). Se manifestă în programe multi-threaded incorect sincronizate, unde un fir poate observa efectele produse de alte fire.

Efectul excluderii reciproce și al sincronizării firelor de execuție, adică funcționarea corectă a acestora se realizează numai prin introducerea unui bloc sincronizat sau a unei metode care dobândește implicit o blocare, sau prin obținerea explicită a unei blocări. Despre asta vom vorbi mai jos. Ambele moduri de lucru vă afectează memoria și este important să nu uitați de lucrul cu variabile volatile .

Câmpuri volatile în Java

Dacă o variabilă este marcată volatilă , este disponibilă la nivel global. Aceasta înseamnă că, dacă un fir accesează o variabilă volatilă , își va obține valoarea înainte de a utiliza valoarea din cache.

O scriere funcționează ca o lansare de monitor, iar o citire funcționează ca o captură de monitor. Accesul se realizează într-o relație de tipul „efectuat înainte”. Dacă vă dați seama, tot ceea ce va fi vizibil pentru firul A atunci când accesează o variabilă volatilă este variabila pentru firul B. Adică, aveți garantat că nu veți pierde modificările de la alte fire.

Variabilele volatile sunt atomice, adică la citirea unei astfel de variabile, se folosește același efect ca și la obținerea unei blocări - datele din memorie sunt declarate nevalide sau incorecte, iar valoarea variabilei volatile este din nou citită din memorie . La scriere, se folosește efectul asupra memoriei, precum și la eliberarea unei blocări - un câmp volatil este scris în memorie.

Java concurent

Dacă doriți să faceți o aplicație super-eficientă și multi-threaded, trebuie să utilizați clasele din biblioteca JavaConcurrent , care se află în pachetul java.util.concurrent .

Biblioteca este foarte voluminoasă și are funcționalități diferite, așa că haideți să aruncăm o privire la ceea ce este în interior și să o împărțim în câteva module:

Java concurent

Concurrent Collections este un set de colecții pentru lucrul într-un mediu cu mai multe fire. În loc de wrapper-ul de bază Collections.synchronizedList cu blocarea accesului la întreaga colecție, se folosesc blocări pe segmentele de date sau algoritmi fără așteptare sunt utilizați pentru a citi datele în paralel.

Cozi - cozi de non-blocare și blocare pentru lucrul într-un mediu cu mai multe fire. Cozile care nu se blochează se concentrează pe viteză și funcționare fără a bloca firele. Cozile de blocare sunt potrivite pentru lucru atunci când trebuie să „încetiniți” firele Producer sau Consumer . De exemplu, într-o situație în care unele dintre condiții nu sunt îndeplinite, coada este goală sau plină sau nu există consumator gratuit 'a.

Sincronizatoarele sunt utilitare utilitare pentru sincronizarea firelor. Sunt o armă puternică în calculul „paralel”.

Executors este un cadru pentru crearea mai convenabilă și ușoară a pool-urilor de fire, este ușor de configurat programarea sarcinilor asincrone cu obținerea de rezultate.

Blocările sunt multe mecanisme flexibile de sincronizare a firelor de execuție în comparație cu sincronizarea de bază , așteptare , notificare , notificare .

Atomii sunt clase care pot suporta operații atomice pe primitive și referințe.