Differences

This shows you the differences between two versions of the page.

Link to this comparison view

apd:laboratoare:06 [2020/11/13 18:05]
radu.ciobanu
apd:laboratoare:06 [2023/10/08 16:32] (current)
dorinel.filip move
Line 1: Line 1:
-===== Laboratorul 06 - Structuri și operații atomice în Java =====+===== Laboratorul 6 - Structuri și operații atomice în Java =====
  
-Responsabili:​ Radu Ciobanu, Carina Deaconu, Florin Mihalache +Documentația ​de laborator ​s-a mutat la [[https://mobylab.docs.crescdi.pub.ro/docs/parallelAndDistributed/introduction|această adresă]].
- +
-==== Semaphore (continuare) ==== +
- +
-Laboratorul trecut, am văzut cum semaforul reprezintă o generalizare a unui mutex. Un semafor iniţializat cu 1 poate fi folosit drept lock, pentru că doar un thread are acces la zona critică la un moment ​de timp. Totuși, un semafor poate avea întrebuinţări mult mai complexe, deoarece poate lua diverse valori, atât negative, cât şi pozitive. +
- +
-Când un semafor este iniţializat cu valoarea pozitivă //x//, vă puteţi gândi că //x// thread-uri au voie să intre în secţiunea critică. Pe măsură ce un thread face **acquire()**,​ //x// este decrementat. Când se ajunge la 0, alte posibile thread-uri care vor să acceseze regiunea critică vor trebui să aştepte până când valoarea semaforului creşte ​la o valoare pozitivă (adică până când iese câte un thread din cele aflate în regiunea critică). +
- +
-Complementar,​ când un semafor este iniţializat cu o valoare negativă cum ar fi -1, se aşteaptă ca cel puţin două thread-uri să facă întâi **release()** (pentru ca valoarea semaforului să crească de la -1 la 1), înainte ca o regiune critică să poată fi accesată (adică un alt thread să poată face **acquire()**). Vă puteţi imagina că un al treilea thread aşteaptă „la semafor” ca alte două thread-uri să îi dea un semnal prin apelul **release()** când şi-au „terminat treaba”. Puteți observa un exemplu în pseudocodul de mai jos. +
- +
-|  <code java>​Semaphore sem = new Semaphore(-1);</​code> ​ ||| +
-^  T0  ^  T1  ^  T2  ^ +
-| <code java> +
-// aşteaptă la semafor +
-// după celelalte 2 thread-uri +
-sem.acquire();​ +
-  +
-  +
-  +
-  +
-// sem = 1, deci poate trece +
-System.out.println( +
-    "Am trecut de semafor!"​);​ +
-</​code>​ | <code java> +
- +
- +
- +
-do_work1();​ +
-sem.release();​ +
-// sem = -1 + 1 = 0 +
-  +
-  +
-  +
-  +
-  +
-</​code>​ | <code java> +
- +
- +
- +
- +
- +
- +
-do_work2();​  +
-sem.release();​ +
-// sem = 0 + 1 = 1 +
-  +
-  +
-</​code>​ | +
- +
-==== AtomicInteger ==== +
- +
-Clasa [[https://docs.oracle.com/​javase/​8/​docs/​api/​java/​util/​concurrent/​atomic/​AtomicInteger.html|AtomicInteger]] poate fi gândită ca o implementare atomică a unui Integer, pentru că reţine o valoare de tip întreg și oferă operații atomice pe eaPoate fi iniţializat cu o valoare sau nu, valoarea default fiind 0. +
- +
-Metodele cele mai folosite sunt **get()** (întoarce valoarea de tip întreg), **set(x)** (îi setează valoarea la //x//), **compareAndSet(y,​ x)** (compară întregul cu //x//, iar, dacă cele două valori sunt egale, setează valoarea la //y//), **addAndGet(delta)** / **getAndAdd(delta)** (incrementează valoarea curentă cu //delta// și returnează valoarea nouă, respectiv pe cea veche). Un exemplu de utilizare a acestor metode poate fi observat mai jos. +
- +
-<code java> +
-AtomicInteger nr = new AtomicInteger(4);​ +
-System.out.println(nr.get());​ // 4 +
- +
-if (nr.compareAndSet(4,​ 7)) +
-    System.out.println(nr.get());​ // nr devine 7 +
- +
-System.out.println(nr.addAndGet(2));​ // se aduna 2, deci devine 9 +
- +
-nr.set(1);​ +
-System.out.println(nr.get());​ // 1 +
-</​code>​ +
- +
-Java are astfel de implementări atomice precum toate tipurile primitive și nu numai (AtomicBoolean,​ AtomicLong, AtomicIntegerArray,​ etc.). Mai multe informații puteți găsi în [[https://docs.oracle.com/javase/8/​docs/​api/​java/​util/​concurrent/​atomic/​package-summary.html|documentația Java]]. +
- +
-==== ConcurrentHashMap ==== +
- +
-[[https://​docs.oracle.com/​javase/​7/​docs/​api/​java/​util/​concurrent/​ConcurrentHashMap.html|ConcurrentHashMap]] este o clasă care are aceleaşi funcţionalităţi ca un HashTable (nu HashMap, pentru că nu permite folosi //null// drept cheie sau valoare). +
- +
-Dacă am folosi un HashMap clasic şi am adăuga un lock pentru accesul la structură, nu ar fi foarte eficient, pentru că doar un singur thread ar putea să o acceseze la un moment de timp, lucru care nu este necesar dacă, de exemplu, ele vor să modifice elemente diferite. Dacă, în schimb, am folosi câte un lock pentru fiecare element, ne-ar trebui destul de multe şi ne-ar îngreuna lucrul. +
- +
-Într-un ConcurrentHashMap,​ operaţiile de tip **get** şi **put** blochează accesul la un anumit element (au mutexuri în implementarea lor), dar **NU** blochează accesul la întreaga structură de date. Cu alte cuvinte, două sau mai multe thread-uri o pot accesa în acelaşi timp, şi operaţiile de tip //get// se pot suprapune cu cele de tip update (**put**), dar implementarea se asigură că **get**-urile se execută la final, după ce s-au executat operațiile de update. +
- +
-Metodele cele mai folosite sunt: +
-  * **get(key)** - returnează valoarea asociată cu o cheie +
-  * **contains(val)** - verifică dacă o valoare există în map +
-  * **containsKey(key)** - verifică dacă o cheie există în map +
-  * **put(key, val)** - adaugă o valoare la cheia specificată +
-  * **putIfAbsent(key,​ val)** - adaugă o asociere doar dacă nu există deja o valoare pentru cheia respectivă;​ este întoarsă valoarea veche, dacă există, altfel e întors //null// +
-  * **remove(key)** - şterge o asociere cheie-valoare +
-  * **replace(key,​ val)** - modifică valoarea asociată cu o cheie. +
- +
-Puteți observa mai jos un exemplu de utilizare. +
- +
-<code java> +
-ConcurrentHashMap<​Integer,​ String> map = new ConcurrentHashMap<​Integer,​ String>​();​  +
- +
-map.put(0, "​Zero"​);​  +
-map.put(1, "​One"​);​ +
- +
-System.out.println(map.get(1));​ // One +
- +
-String val = map.putIfAbsent(1,​ "​Unu"​);​ // nu se modifică nimic, val va fi "​Unu"​ +
- +
-val = map.putIfAbsent(2,​ "​Two"​);​ // se adaugă maparea 2-"​Two",​ val va fi null +
-</​code>​ +
- +
-==== BlockingQueue ==== +
- +
-Interfaţa [[https://​docs.oracle.com/​javase/​8/​docs/​api/​java/​util/​concurrent/​BlockingQueue.html|BlockingQueue]] extinde interfaţa Queue, clasele care o implementează fiind potrivite pentru programe paralele, în care pot exista probleme de concurenţă. Dintre acestea, cele mai relevante sunt ArrayBlockingQueue,​ LinkedBlockingQueue şi PriorityBlockingQueue. +
-Toate simulează o structură de tip FIFO (coadă), cu operaţii thread-safe.  +
- +
-Diferenţa este la implementarea din spate: ArrayBlockingQueue e bazată pe un array, LinkedBlockingQueue pe o listă înlănţuită,​ iar pentru PriorityBlockingQueue poate fi specificat un comparator la iniţializare,​ păstrând o ordine în care să fie elementele. Niciuna dintre clase nu acceptă elemente de tip //null// şi li se poate specifica o capacitate maximă la iniţializare (pentru ArrayBlockingQueue este obligatoriu acest lucru). +
- +
-Operaţiile sunt pe principiul FIFO (se inserează la final şi se extrage de la începutul cozii), așa cum se poate observa în tabelul de mai jos. +
- +
-^  **Operație** ​ ^  **Aruncă excepție** ​ ^  **Valoare specială** ​ ^  **Se blochează** ​ ^  **Dă timeout** ​ ^ +
-|  Inserare ​ |   ​add(e) ​ |   ​offer(e) ​ |   ​put(e) ​ |   ​offer(e,​ time, unit)  | +
-|  Ștergere ​ |   ​remove() ​ |   ​poll() ​ |   ​take() ​ |   ​poll(time,​ unit)  | +
-|  Examinare ​ |   ​element() ​ |   ​peek() ​ |   ​- ​ |   ​- ​ | +
- +
-Deşi sunt mai multe metode pentru aceeaşi operaţie asemănătoare,​ comportamentul lor este diferit. Cea mai importantă diferenţă o constituie proprietatea de a fi sau nu blocantă. De exemplu, **add(obj)** adaugă un element doar dacă este loc în coadă (adică dacă nu s-a atins capacitatea maximă) şi returnează //true// dacă operaţia a reuşit, sau IllegalStateException în caz contrar. În schimb, **put(obj)** se blochează dacă nu este spaţiu suficient în coadă şi aşteaptă până poate pune elementul cu succes. +
- +
-==== Collections.synchronized ==== +
- +
-Dacă dorim să lucrăm cu colecții clasice, precum ArrayList, și să avem operațiile sincronizate,​ putem să le instanțiem folosind: +
- +
-  * Collections.synchronizedList (pentru liste): <code java>​List<​Integer>​ syncList = Collections.synchronizedList(new ArrayList<>​());</​code>​ +
-  * Collections.synchronizedMap (pentru dicționare):​ <code java>​Map<​Integer,​ String> syncMap = Collections.synchronizedMap(new HashMap<>​());</​code>​ +
- +
-==== Arbori concurenți ==== +
- +
-Atunci când lucrăm cu structuri mai complexe, trebuie să avem grijă la sincronizarea accesului concurent la ele, dar și la eficiența finală a implementării. Ca să vedem un exemplu concret, vom presupune că avem un arbore binar. Când se dorește inserarea unui element într-un astfel de arbore, se verifică mai întâi dacă nodul unde inserăm are copil în partea stângă. Dacă are, atunci nodul copil va fi inserat în stânga, altfel va fi inserat în partea dreaptă: +
- +
-<code java> +
-if (node.left == null) +
-    node.left = child; +
-else +
-    node.right = child; +
-</​code>​ +
- +
-Când mai multe thread-uri acționează asupra unui arbore binar, pot apărea probleme de concurență la inserare. Un astfel de scenariu ar putea avea loc atunci când două thread-uri încearcă să insereze simultan câte o valoare diferită în același nod din arbore: +
- +
-^  **T0** ​ ^  **T1** ​ ^ +
-|  Verifică dacă există copil în stânga ​ |   ​Verifică dacă există copil în stânga ​ | +
-|  Condiția este adevărată,​ trece în prima ramură a if-ului ​ |   ​Condiția este adevărată,​ trece în prima ramură a if-ului ​ | +
-|  Inserează nodul în stânga ​ |    | +
-|    |  Inserează nodul în stânga -> se suprascrie valoarea scrisă de **T0** ​ | +
- +
-În acest caz, corect ar fi ca unul din thread-uri să insereze în stânga și celălalt să insereze în dreapta. O soluție este să folosim un lock pe operația de inserare unui nod. Dacă folosim un lock global (pentru tot arborele), două thread-uri nu pot insera noduri copii în același timp pentru două noduri parinte diferite. Din acest motiv, mai eficient ar fi să existe câte un lock pentru fiecare nod. +
- +
-==== Exerciții ==== +
- +
-  - Pornind de la arhiva de laborator, rezolvați problema de sincronizare din pachetul //​multipleProducersMultipleConsumers//,​ folosind ArrayBlockingQueue. **(2p)** +
-  - Rezolvați problema de sincronizare din pachetul //​synchronizationProblem//,​ folosind un AtomicInteger. **(2p)** +
-  - Rezolvați problema de sincronizare din pachetul //​bugConcurrentHashMap//,​ folosind metode din clasa ConcurrentHashMap. **(2p)** +
-  - Paralelizați programul din pachetul //​synchronizedSortedList//,​ unde: +
-    - trei thread-uri citesc numere din trei fișiere (câte un thread citește câte un fișier) și adaugă numerele din fișiere într-o listă partajată +
-    - al patrulea thread sortează lista cu numerele +
-    - lista poate fi sortată **doar** după ce citirea a fost efectuată de către toate cele trei thread-uri care citesc din fișier +
-    - sincronizarea între thread-uri trebuie realizată folosind un semafor. **(2p)** +
-  - Paralelizați programul din pachetul //​parallelTree//,​ unde: +
-    - două thread-uri construiesc un arbore binar în paralel, citind ID-urile și părintele nodurilor noi din câte un fișier, și apoi executând operația de inserare +
-    - al treilea verifică dacă arborele a fost construit corect +
-    - verificarea arborelui nu se poate face decât după finalizarea alcătuirii sale +
-    - sincronizarea între thread-uri trebuie realizată folosind o barieră +
-    - la inserția în arbore, sincronizarea nu trebuie făcută global (pe tot arborele), ci pentru fiecare nod din arbore**(2p)**+
apd/laboratoare/06.1605283503.txt.gz · Last modified: 2020/11/13 18:05 by radu.ciobanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0