Java - II.¶
A gyakorlat anyaga¶
Ebben az anyagben az olvasók-írók, majd az étkező filozófusok problémáján keresztül nézünk példákat a kölcsönös kizárásra. Továbbá megismerünk néhány magasabb szintű Java párhuzamos konstrukciót, amelyet a concurrent package biztosít.
Olvasók-írók probléma¶
A probléma definíciója szerint adott egy könyvtár, ahol egyidejűleg több olvasó is tölheti az idejét olvasással, viszont egyidőben csak egy író írhatja a könyvet (férhez hozzá az erőforráshoz). Ekkor olvasó sem olvashat. Továbbá az egyidőben olvasók száma korlátozva van. 4 esetre kell odafigyelni: mikor egy olvasó belép, mikor egy olvasó kilép, illetve mikor egy író belép majd kilép. Az egyes belépés/kilépés párok (amelyek lényegében a kritikus szekciók) között a párhuzamos futtatás megengedett (hiszen az olvasók egy limitált száma párhuzamosan hozzáférhet az erőforráshoz).
A megvalósításhoz továbbra is a Lock és a Condition interfészeket fogjuk használni. Érdekesség, hogy az olvasók-írók problémára egy speciális Lock implementációt is publikáltak: ReadWriteLock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
Az étkező filozófusok probléma¶
A probléma definíciója szerint adott n db filozófus, akik gondolkodnak, majd eközben megéheznek. Adott egy asztal n db tányérral, illetve minden tányér között van egy-egy villa. Ahhoz hogy egy filozófus enni tudjon, fel kell vennie a tányérja jobb és bal oldalán található 1-1 villát. Azaz a verseny a villákért folyik, a villák reprezentálják az erőforrást. Ha egy filozófus befejezte az evést, akkor visszarakja a villákat az asztalra, amit így a szomszédos tányéroknál elhelyezkedő filozófusok használhatnak.
Ennek megvalósításához egy újabb (de már korábbról ismert) konstrukciót használunk, ez lesz a Semaphore osztály. amelynek az acquire() és a release() metódusai felelnek majd meg a wait() és signal() utasításoknak.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
A holtpont elkerülésének érdekében 2 megközelítéssel dolgozhatunk:
-
Kijelölünk egy filozófust, aki fordított sorrendben (pl. először a bal, majd a jobb oldalról) feszi fel a villákat, mindenki más a jobb, majd bal sorrendet követi.
-
Egy új, nem-bináris szemafort hozunk létre, amely guard-ként működik. Értékét N-1-re állítjuk, ahol N a filozófusok száma. Minden filozófus először a guard szemafortól kér engedélyt, hogy odalépjen az asztalhoz.
Összefoglaló táblázat¶
Megvalósítás | WAIT | SIGNAL |
---|---|---|
Implicit monitor | wait() | notify() / notifyAll() |
Lock + Condition | await() | signal() / signalAll() |
Semaphore | acquire() | release() |
Atomic¶
Az atomic package több típusra is biztosít nekünk szálbiztos hozzáférést, többek között: AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference. Cseréljük le a Counter
osztályunkban lévő adattagot egy AtomicInteger
objektumra, a dokumentációban pedig keressük ki a inkrementáláshoz használható függvényeket.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Barrier¶
A CyclicBarrier osztály lehetőséget biztosít, hogy több szál bevárja egymást a futásának egy bizonyos pontján.
A ciklikusság arra a tulajdonságára hivatkozik, hogy a várakoztatott szálak felszabadítását követően újra használhatóvá válik a Barrier (lásd reset()
metódus a dokuementációban).
Ennek segítségével szinkronizálhatjuk a szálakat egy ún. barrier
pontban. A funkcionalitás különlegessége, hogy a konstruktora kiegészíthető egy Runnable()
interfésszel (lásd dokumentáció a konstruktorokról), ami pontosan akkor és pontosan egyszer fut le, amikor az utolsó szál is megérkezett az ún. barrier
pontba, de még mielőtt bármelyik is folytatná a futását.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
ExecutorService¶
Az ExecutorService segítségével feladatokat tudunk végrehajtani. Ehhez az Executors osztályt fogjuk használni, amellyel különböző stratégiákkal működő szálakat tudunk létrehozni, amelyek feldolgozzák a feladatokat. Ilyen stratégiákat, amelyeket ún. thread pool-ok valósítanak meg és újrafelhasználható szálakat biztosít, a newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor segítségével tudunk létrehozni.
Egy feladatot egy Runnable vagy a Callable osztály példányai reprezentálnak. Utóbbinak az előnye, hogy a Callable rendelkezik visszatérési értékkel, amely különösen jó lehet akkor, ha valamilyen részfeladat eredményével szeretnék visszatérni. Ehhez a [Future][future] interfészt fogjuk használni, amely aszinkron módon képes kezelni a feladatokat és visszatérni az eredményükkel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Gyakorló feladat¶
I) Adott egy speciális nyomtató (erőforrás), amely egyidőben képes kiszolgálni két különböző típusú kérést: nyomtatni és fénymásolni. Továbbá, a nyomtató speciális felépítéséből adódóan egyszerre két dokumentumot is képes nyomtatni.
A nyomtatások minden esetben 400 ms-ig, a fénymásolás pedig 550 ms-ig tart (altatás). Összesen 20-20 nyomtatási és fénymásolási kérés érkezett be (szálak).
Amennyiben éppen két nyomtatás zajlik, a következő nyomtatási szálnak várakoznia kell. Hasonlóan, amennyiben fénymásolás zajlik, a következő fénymásolási szálnak várakoznia kell. Figyelj a kölcsönös kizárásra és a párhuzamosan futtatható részekre, csak és kizárólag a szükséges kódrészlet legyen része a kritikus szakasznak. Amennyiben van várakozó nyomtatási/fénymásolási kérés, és a megfelelő erőforrás szabad, ne várakoztasd feleslegesen a folyamatot.
A kölcsönös kizárás megvalósításához a Semaphore osztályt használd. A megoldásodat a join metódus és CyclicBarrier használata nélkül készítsd el, és csak a feladat által kért esetben használj altatást.
II) Valósítsd meg az előző összefoglaló végén részletezett Pi közelítését ExecutorService, Callable és Future segítségével. A Callable egy Point objektummal térjen vissza. Az Executor akkor kerüljön leállításra, amíg egy adott epszilon hibahatáron belülre kerül a Pi közelített értéke.