Concurență în Java

Limbajul de programare Java și JVM ( Java Virtual Machine ) sunt proiectate pentru a suporta calculul paralel și toate calculele sunt efectuate în contextul unui fir de execuție . Firele multiple pot partaja obiecte și resurse; fiecare fir execută propriile instrucțiuni (cod), dar poate accesa orice obiect din program. Este responsabilitatea programatorului să coordoneze (sau să „ sincronizeze ”) firele în timpul operațiunilor de citire și scriere pe obiecte partajate. Sincronizarea firelor de execuție este necesară pentru a se asigura că doar un fir de execuție poate accesa un obiect la un moment dat și pentru a preveni accesul firelor de execuție la obiecte actualizate incomplet în timp ce un alt fir de execuție lucrează la ele. Limbajul Java are încorporate constructe de suport pentru sincronizarea firelor.

Procese și fire

Cele mai multe implementări ale mașinii virtuale Java utilizează un singur proces pentru a rula programul, iar în limbajul de programare Java, calculul paralel este cel mai frecvent asociat cu firele de execuție . Firele sunt uneori numite procese ușoare .

Stream obiecte

Threadurile partajează resurse de proces, cum ar fi memoria și fișierele deschise, între ele. Această abordare duce la o comunicare eficientă, dar potențial problematică. Fiecare aplicație are cel puțin un fir de execuție. Firul din care începe execuția programului se numește main sau main . Firul principal este capabil să creeze fire suplimentare sub formă de obiecte Runnablesau Callable. (Interfața Callableeste similară prin Runnablefaptul că ambele sunt concepute pentru clase care vor fi instanțiate pe un fir separat. RunnableCu toate acestea, nu returnează un rezultat și nu poate arunca o excepție verificată .)

Fiecare thread poate fi programat să ruleze pe un nucleu separat al procesorului, să folosească time slicing pe un singur nucleu de procesor sau să folosească time slicing pe mai multe procesoare. În ultimele două cazuri, sistemul va comuta periodic între fire, permițând alternativ executarea unuia sau celuilalt. Această schemă se numește pseudo-paralelism. Nu există o soluție universală care să spună exact cum vor fi convertite firele Java în fire native ale sistemului de operare. Depinde de implementarea JVM specifică.

În Java, un fir este reprezentat ca un obiect copil al fișierului Thread. Această clasă încapsulează mecanisme standard de threading. Threadurile pot fi gestionate fie direct, fie prin mecanisme abstracte, cum ar fi Executor și colecții din pachetul java.util.concurrent.

Rularea unui thread

Există două moduri de a începe un nou thread:

  • Implementarea interfeței Runnable
public class HelloRunnable implementează Runnable { public void run () { System . afară . println ( "Bună ziua din fir!" ); } public static void main ( String [] args ) { ( thread nou ( nou HelloRunnable ())). începe (); } }
  • Moștenirea din clasa Thread
public class HelloThread extinde Thread { public void run () { System . afară . println ( "Bună ziua din fir!" ); } public static void main ( String [] args ) { ( nou HelloThread ()). începe (); } } Întreruperi

O întrerupere este o indicație către un fir că ar trebui să oprească activitatea curentă și să facă altceva. Un thread poate trimite o întrerupere apelând metoda interrupt() a obiectului Threaddacă trebuie să întrerupă firul asociat. Mecanismul de întrerupere este implementat folosind starea de întrerupere a flagului intern (steagul de întrerupere) al clasei Thread. Apelarea Thread.interrupt() ridică acest flag. Prin convenție, orice metodă care se termină cu o excepție InterruptedException va reseta indicatorul de întrerupere. Există două moduri de a verifica dacă acest steag este setat. Prima modalitate este de a apela metoda bool isInterrupted() a obiectului thread, a doua modalitate este de a apela metoda statică bool Thread.interrupted() . Prima metodă returnează starea steagului de întrerupere și lasă acest steag neatins. A doua metodă returnează starea steagului și o resetează. Rețineți că Thread.interrupted()  este o metodă statică a clasei Thread, iar apelarea acesteia returnează valoarea steagului de întrerupere a firului de execuție de la care a fost apelat.

Se așteaptă finalizarea

Java oferă un mecanism care permite unui fir să aștepte ca un alt fir să se termine de execuție. Pentru aceasta, se folosește metoda Thread.join() .

Demoni

În Java, un proces se termină când ultimul său fir de execuție se termină. Chiar dacă metoda main() s-a finalizat deja, dar firele pe care le-a generat încă rulează, sistemul va aștepta să se finalizeze. Cu toate acestea, această regulă nu se aplică unui tip special de fir - demoni. Dacă ultimul fir normal al procesului s-a încheiat și rămân doar fire de execuție demon, acestea vor fi oprite forțat și procesul se va încheia. Cel mai adesea, firele daemon sunt folosite pentru a efectua sarcini de fundal care deservesc un proces pe durata de viață.

Declararea unui fir ca demon este destul de simplă - trebuie să apelați metoda setDaemon(true) înainte de a începe firul ; Puteți verifica dacă un fir este un daemon apelând metoda booleană isDaemon() .

Excepții

O excepție aruncată și netratată va duce la terminarea firului de execuție. Firul principal va imprima automat excepția pe consolă, iar firele create de utilizator pot face acest lucru numai prin înregistrarea unui handler. [1] [2]

Model de memorie

Modelul de memorie Java [1] descrie interacțiunea firelor de execuție prin intermediul memoriei în limbajul de programare Java. Adesea, pe computerele moderne, codul nu este executat în ordinea în care este scris de dragul vitezei. Permutarea este realizată de compilator , procesor și subsistemul de memorie . Limbajul de programare Java nu garantează atomicitatea operațiilor și consistența secvențială atunci când citiți sau scrieți câmpuri ale obiectelor partajate. Această soluție eliberează mâinile compilatorului și permite optimizări (cum ar fi alocarea registrului , eliminarea subexpresiilor comune și eliminarea operațiunilor de citire redundante ) bazate pe permutarea operațiunilor de acces la memorie. [3]

Sincronizare

Firele de execuție comunică prin partajarea accesului la câmpuri și obiecte la care se face referire prin câmpuri. Această formă de comunicare este extrem de eficientă, dar face posibile două tipuri de erori: interferența firelor și erorile de consistență a memoriei. Pentru a preveni apariția lor, există un mecanism de sincronizare.

Reordonarea (reordonarea, reordonarea) se manifestă în programe cu mai multe fire sincronizate incorect , în care un fir poate observa efectele produse de alte fire și astfel de programe pot fi capabile să detecteze că valorile actualizate ale variabilelor devin vizibile pentru alte fire într-un alt fire. ordine decât cea specificată în codul sursă.

Pentru a sincroniza firele de execuție în Java, se folosesc monitoare , care sunt un mecanism de nivel înalt care permite doar unui fir de execuție odată să execute un bloc de cod protejat de un monitor. Comportamentul monitoarelor este considerat în termeni de încuietori ; Fiecare obiect are asociat o lacăt.

Sincronizarea are mai multe aspecte. Cel mai bine înțeles este excluderea reciprocă - doar un fir de execuție poate deține un monitor, astfel sincronizarea pe monitor înseamnă că odată ce un fir de execuție intră într-un bloc sincronizat protejat de monitor, niciun alt thread nu poate intra în blocul protejat de acest monitor până la primul fir de execuție. iese din blocul sincronizat.

Dar sincronizarea este mai mult decât o simplă 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 blocul sincronizat, eliberăm monitorul, ceea ce are ca efect golirea memoriei cache în memoria principală, astfel încât scrierile făcute de thread-ul nostru să poată fi vizibile pentru alte fire. Înainte de a putea intra în blocul sincronizat, achiziționăm monitorul, ceea ce are ca efect invalidarea cache-ului procesorului local astfel încât variabilele să fie încărcate din memoria principală. Apoi putem vedea toate intrările făcute vizibile de ediția anterioară a monitorului. (JSR 133)

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.

Blocări și blocuri sincronizate

Efectul excluderii reciproce și al sincronizării firelor de execuție se realizează prin introducerea unui bloc sincronizat sau a unei metode care dobândește blocarea implicit sau prin achiziționarea explicit a blocării (cum ar fi ReentrantLockdin pachetul java.util.concurrent.locks). Ambele abordări au același efect asupra comportamentului memoriei. Dacă toate încercările de acces la un anumit câmp sunt protejate de aceeași blocare, atunci operațiile de citire-scriere ale acestui câmp sunt atomice .

Câmpuri volatile

Când este aplicat câmpurilor, cuvântul cheie volatilegarantează:

  1. (În toate versiunile de Java) Accesurile la volatile-variable sunt ordonate global. Aceasta înseamnă că fiecare fir care accesează câmpul - volatileîși va citi valoarea înainte de a continua, în loc să folosească (dacă este posibil) valoarea din cache. (Accesurile la volatile-variabile nu pot fi reordonate între ele, dar pot fi reordonate cu acces la variabile obișnuite. Acest lucru anulează utilitatea volatilecâmpurilor - ca mijloc de semnalizare de la un fir la altul.)
  2. (În Java 5 și versiuni ulterioare) Scrierea într-un câmp are volatileacelași efect asupra memoriei ca eliberarea monitorului , în timp ce citirea are același efect ca și achiziția monitorului .  Accesarea câmpului - stabilește relația se întâmplă înainte. [4] În esență, această relație este o garanție că orice era vizibil pentru fir atunci când a scris în câmpul -devine vizibil pentru fir atunci când citește . volatile AvolatilefBf

Volatile-câmpurile sunt atomice. Citirea dintr volatile-un câmp are același efect ca și obținerea unei blocări: datele din memoria de lucru sunt declarate nevalide, iar valoarea volatilecâmpului este recitit din memorie. Scrierea într-un volatilecâmp are același efect asupra memoriei ca și eliberarea unei blocări: volatile-câmpul este imediat scris în memorie.

Câmpuri finale

Un câmp care este declarat final se numește final și nu poate fi modificat după inițializare. Câmpurile finale ale unui obiect sunt inițializate în constructorul său. Dacă constructorul urmează anumite reguli simple, atunci valoarea corectă a câmpului final va fi vizibilă pentru alte fire fără sincronizare. O regulă simplă este că această referință nu trebuie să părăsească constructorul până la finalizare.

Istorie

Începând cu JDK 1.2 , Java include un set standard de clase de colecție Java Collections Framework .

Doug Lee , care a contribuit și la implementarea Java Collections Framework, a dezvoltat pachetul de concurență , care include mai multe primitive de sincronizare și un număr mare de clase legate de colecții. [5] Lucrările la acesta au continuat ca parte a JSR 166 [6] sub președinția lui Doug Lee .

Versiunea JDK 5.0 a inclus multe completări și clarificări la modelul de concurență Java. Pentru prima dată, API-urile de concurență dezvoltate de JSR 166 au fost incluse în JDK. JSR 133 a oferit suport pentru operațiuni atomice bine definite într-un mediu cu mai multe fire/multiprocesor.

Atât Java SE 6 , cât și Java SE 7 aduc modificări și completări la API-ul JSR 166.

Vezi și

Note

  1. Oracle Interface Thread.UncaughtExceptionHandler . Preluat la 10 mai 2014. Arhivat din original la 12 mai 2014.
  2. Moartea Silent Thread din cauza excepțiilor necontrolate . readjava.com . Preluat la 10 mai 2014. Arhivat din original la 12 mai 2014.
  3. Herlihy, Maurice și Nir Shavit. „Arta programării multiprocesor”. PODC. Vol. 6. 2006.
  4. Secțiunea 17.4.4: Ordinea de sincronizare Specificația limbajului Java®, ediția Java SE 7 . Oracle Corporation (2013). Preluat la 12 mai 2013. Arhivat din original la 3 februarie 2021.
  5. Doug Lee . Prezentare generală a pachetului util.concurrent Versiunea 1.3.4 . — « Notă: La lansarea J2SE 5.0, acest pachet intră în modul de întreținere: vor fi eliberate numai corecțiile esențiale. Pachetul J2SE5 java.util.concurrent include versiuni îmbunătățite, mai eficiente, standardizate ale componentelor principale din acest pachet. ". Preluat la 1 ianuarie 2011. Arhivat din original la 18 decembrie 2020.
  6. JSR 166: Utilitare de concurență (link nu este disponibil) . Consultat la 3 noiembrie 2015. Arhivat din original pe 3 noiembrie 2016. 

Link -uri

  • Goetz, Brian; Iosua Bloch; Joseph Bowbeer; Doug Lea; David Holmes Tim Peierls. Concurența Java în practică  (neopr.) . - Addison Wesley , 2006. - ISBN 0-321-34960-1 .
  • Leah, Doug. Programare concomitentă în Java : principii și modele de proiectare  . - Addison Wesley , 1999. - ISBN 0-201-31009-0 .

Link-uri către resurse externe