1. Elosztott, párhuzamos és konkurens programok. Többszálúság. Elosztott programokat hálózattal összekötött számítógépeken futtathatunk. Ez a megoldás a Java EE alapja, mely a távoli metódushívásokra épít (RMI). Párhuzamos programokat futtathatunk elosztott memória és megosztott memória felett is. Egy konkurens programban több végrehajtási stream hajtódik végre konkurensen. Ezeket az utasítás szekvenciákat hívjuk szálaknak. Minden ilyen szál ugyanúgy hajtódik végre, mint egy szekvenciális program azzal a különbséggel, hogy a szálak képesek egymással kommunikálni és interferálni. Többszálú program esetén a szálak átfedik egymást előre meg nem jósolható módon. 2. Elosztott és megosztott memória a folyamatok közötti kommunikációban. Megosztott memória esetén két vagy több folyamat ugyanazt a memóriarészt látja. A közös memóriaterületen közös változók segítségével érhető el a leggyorsabb folyamatok közötti kommunikáció, hiszen nem kell mozgatni az adatokat. Ha több processzorunk van és minden processzornak saját memóriája és címtartománya van, elosztott memóriáról beszélhetünk. Itt akár fizikailag másik gépben is lehetnek a processzormemória párosok. A folyamtok közötti kommunikáció lassú, üzenetekkel történik. 3. Megosztott memória a szálkezelésben: stackek, heap. Minden szálnak saját stack-je van ahol a csak a szálban látható, lokális változók kapnak helyet. A heap-en foglalt változók megoszthatók a szálak között. 4. Szálak definiálása és elindítása: Thread és Runnable. Két féle módszerrel definiálhatunk szálakat Java-ban: A Thread osztályból származtatjuk az osztályunkat Ekkor felüldefiniáljuk a Thread osztály run metódusát. Az osztályunknak lesz egy start metódusa, amit meghívva elindíthatjuk a run végrehajtását egy külön szálban. Megvalósítjuk a Runnable interface-t Sokszor már másik osztályból származtatunk és ilyenkor ezzel az interface-el is futtatható szállá tehetjük az osztályunkat. Meg kell valósítanunk a run metódust. A futatáshoz egy Thread osztály konstruktorának kell átadni a Runnable osztályunkat, amin aztán meghívhatjuk a start metódust a szál elindításához. 5. A java.lang.Thread osztály műveletei. A főbb műveletek: interrupt() – félbeszakítja a szál végrehajtását interrupted() – statikus metódus, ellenőrzi, hogy az aktuális szálon meghívták-e az interrupt()-ot isAlive(), isInterrupted() – egy adott szálat tesztel join()- vár az adott szálra, hogy befejeződjön sleep(long millis) – az aktuálisan futó szál felfüggesztése egy adott ideig (millis) start() – elindítja a thread-et, a JVM meghívja a run metódust yield() – az aktuálisan futó szál átmenetileg megáll, visszaadja a vezérlést az ütemezőnek 6. Szálak életciklusa. Miután a programozó létrehozza és elindítja az új szálat az vagy Runnable (Ready-to-run) állapotba kerül és várakozik, hogy sorra kerüljön az ütemezőnél. Ha az ütemező kiválasztja a szálat az állapota Running-ra változik. Ekkor elindul a végrehajtása. Innen Blocked állapotba akkor kerülhet, ha pl. erőforrásra vár. Innen visszakerülhet Runnable-be, ha elérhetővé vált az erőforrás. A Dead állapotba akkor kerül egy szál, ha a run metódusa végére ért. Running és Blocked állapotból is elérhető a Dead állapot.
Ha egy magasabb prioritású folyamat futtatható állapotba kerül, az aktuális alacsonyabb prioritású folyamat felfüggesztődik (pre-emption) és végrehajtásra kerül ez a folyamat. Ha egy szál elkezdett futni, akkor addig fut, amíg az alábbiak valamelyike be nem következeik: Thread.sleep lock-ra kezd várni, hogy egy synchronized metódust futtasson I/O-n blokkolódik Explicit visszaadja a vezérlést a yield-el terminál 7.
Blokkoló tevékenységek. Synchronization actions, which are: o Volatile read. A volatile read of a variable. o Volatile write. A volatile write of a variable. o Lock. Locking a monitor o Unlock. Unlocking a monitor. o The (synthetic) first and last action of a thread. o Actions that start a thread or detect that a thread has terminated (§17.4.4).
8. Ütemezés: run-to-completion, pre-emption. Időosztás. Minimális időszelet. run-to-completion Egy folyamatot addig futtatunk, amíg kész nem lesz, vagy explicit vissza nem adja a vezérlést az ütemezőnek (yield) pre-emption A folyamtok felfüggeszthetők és az ütemező választja ki a következő folyamatot. Ez a váltás a kontextusváltás. Időosztás A JVM implementációtól függően alkalmazhat időosztást az azonos prioritású szálak futtatásakor. Ekkor a szálak adott időszeleteket kapnak a végrehajtásra és kontextus váltásokat hajt végre köztük a JVM az idő lejártakor, ha a szálak még mindig futtatható állapotban maradtak. Minimális időszelet ??? 9. Kontextusváltás és a vele járó költségek. Kontextusváltásról akkor beszélünk, mikor egy folyamat helyett a processzor egy másik folyamatot kezd el futtatni. A folyamtok közötti váltás több költséggel is jár: sok CPU-idő (akár több ezer órajel) kellhet a váltáshoz több cache miss a váltás után, mivel a cache-ek elavultak lesznek túl gyakori blokkolódásnál gyakran vész el az időszelet 10. Szálak közötti interferencia. Race condition. Szálbiztosság. Szálak közötti interferenciáról akkor beszélünk, mikor két műveletet ugyanazon az adaton külön szálakból meghívva összefésülődés történik, vagyis a két művelet több lépésből áll és ezek között átfedés alakul ki. Race condition akkor lép fel mikor két vagy több szál ugyanazt az erőforrást próbálja használni. Egy szálbiztos osztály nem hibásabb konkurens szituációban, mint egyszálú környezetben. Ilyen pl. egy állapotmentes, vagy egy módosíthatatlan objektum. 11. Monitorok működése. A Java synchronized kulcsszóval elérhető szinkronizációt monitor segítségével valósították meg.
Minden objektum rendelkezik egy monitorral amin egy szál lock-olhat vagy unlock-olhat. Egyszerre csak egy szál tarthatja a monitor lock-ját. Minden más szál blokkolt állapotba kerül és egy várakozási sorban vár addig amíg nem tudnak lock-ot szerezni a monitoron. Egy monitort többször is lehet lock-olni, minden unlock „csökkenti a lock-ok számát”. 12. A volatile szerepe. A volatile kulcsszó segítségével szinkronizációt tudunk kikényszeríteni explicit lokkolás nélkül. A fordítónak mindig ki kell olvasnia a volatile változó értékét így nem fordulhat elő az, hogy egy cache-elt elavult értéket használunk. Ez azt jelenti, hogy egy szálak között megosztott volatile változónak mindig az aktuális értékét látjuk minden szálban. A volatile read és volatile write szinkronizációs akciót vált ki. Egy volatile mező írása happens-before relációban van minden ezt az írást követő olvasással az adott mezőn. Egy volatile long vagy double változó írása és olvasása mindig atomi. 13.
Optimalizációk által okozott anomáliák. Kód átrendezése. Cache. Word-tearing.
pl. A és B megosztott, r1 és r2 lokális változók A = B = 0 I. II. r2 = A r1 = B B = 1 A = 2 i, r2 == 2 & r1 == 1 Intra-thread vagyis egy thread-en belül biztosított a szekvenciális konzisztencia. Ez azt jelenti, hogy a thread-en belül a fordító optimalizálva pl. úgy rendezheti át a sorokat, hogy az nem változtat az intra-thread szemantikán. Így előfordulhat az, hogy a fordító az I. szálban előbb hajtja végre a B=1 utasítást, és így ha I. után jön II., akkor i lehetséges. Egy másfajta optimalizáció lehet az, ha a fordító cache-el lekérdezett értékeket és azokat újrahasznosítja, ha úgy látja, hogy intra-thread nem változott az értéke. Ez persze nem jelenti azt, hogy inter-thread sem változott, ami egy rosszul szinkronizált programnál inkonzisztenciához vezethet. Word-tearing: Ha van egy pl. 10 elemű byte tömböm és meg akarom változtatni mondjuk a 3. elemét, akkor arra számítok, hogy csak ez a 3. elem kerül módosításra a művelet során. A legtöbb processzor nem nyújt lehetőséget a byte-onkénti írásra, helyette word-öt kell olvasnunk és írnunk. Ez általában 4 byte, ami azt jelenti, hogy 1 elem helyett 4 elemre lenne hatással a változtatásunk a tömbben. Ez konkurens szálak esetén inkonzisztens állapothoz vezethetne. A JVM szerencsére garantálja az elemek vagy mezők írásának és olvasásának szeparációját, így a word tearing nem történhet meg a Java Language Specification szerint. 14. Memóriamodell. A happens-before reláció. Szekvenciális konzisztencia. A memóriamodell megadja, hogy ha van egy programunk és egy végrehajtási sorozatunk, akkor ez a végrehajtási sorozat a program egy engedélyezett végrehajtása-e. Nem azt adja meg hogyan kell többszálú alkalmazást végrehajtani, hanem egy biztosítékot ad arra, hogy mik azok az esetek amik megtörténhetnek. A happens-before reláció két akció sorrendjét tudja megadni. Ha egy akció „happens-before” egy másik akció, vagyis pl. hb(a1, a2), akkor a1 látható a2 számára és elé van rendezve. Programszövegbeli sorrendiség (részbenrendezés) – po Szinkronizációs események sorrendje adott (teljes rendezés) Synchronizes-with reláció (részbenrendezés) – sw
A szekvenciális konzisztencia amit el szeretnénk érni párhuzamos szálak esetén is, vagyis, hogy összefésülhetőek legyenek a végrehajtott műveleteik és szemantikusan úgy tűnjön, mintha a párhuzamos szálak utasításait szekvenciálisan hajtottuk volna létre. Erre nem nyújt garanciát a Java memória modell, de ha nem tartalmaz data race-t a programunk, akkor szekvenciálisan konzisztens. 15. Jól szinkronizált program. Data race. Légből kapott értékek. Egy program jól szinkronizált akkor és csak akkor, ha nem szerepel data race a szekvenciálisan konzisztens végrehajtásokban. Ha egy program jól szinkronizált, minden végrehajtása szekvenciálisan konzisztensnek tűnik. Ha nincs data race, minden végrehajtás szekvenciálisan konzisztensnek tűnik Data race: two or more threads in a single process access the same memory location concurrently, and at least one of the accesses is for writing, and the threads are not using any exclusive locks to control their accesses to that memory. Légből kapott értéket kaphatunk, ha több szálú programunkat a fordító optimalizálja. A fordító átrendezheti a sorokat. A long és double esetén nem atomi az olvasás/írás. 16. Atomicitás, kizárás és láthatóság biztosítása szinkronizációval. 4 byte-onként kötelező az atomicitás Java-ban. (Probléma long és double esetben.) Láthatóság biztosítható volatile változók használatával, mivel ezeket szálak közt megosztva konzisztens (nem elévült) értéket látunk. 17. Check-then-act. Invariáns több változó felett. check-then-act művelet: Lekérdezzük az aktuális állapotot, majd ennek függvényében cselekszünk. A ConcurrentMap egy olyan adatszerkezet, mely atomi putIfAbsent, remove és replace check-then-act műveleteket biztosít. 18. Belső állapot kiszökése. Biztonságos létrehozás és nyilvánosságra hozás. Mindenki idegen, aki az osztályunkon kívül van, ellenük meg kell védeni a belső állapotot. Összetett osztály esetén is kell lennie egy interface-nek, amire azt mondom, hogy azon kívül mindenki idegen. Pl. idegen műveletnek this-t adni TILOS! Ha csak egy kis részt adunk magunkból, az is megsérti az enkapszulációt. Ezek mind-mind konkurencia problémához vezetnek Biztonságos nyilvánosságra hozás class X { int n = 1; } X v = new X(); Vagy 0 az n, vagy 1 egy másik szálból nézve -- race-condition van u.e. volatile mezővel class X { volatile int n = 1; } X v = new X(); Ezt garantálja az intra-thread szemantika. A külső szálból úgy látszik, mintha előbb kapna értéket az n, mint hogy létrejött az objektum. 19. A final mezők szerepe. Módosíthatatlan (immutable) objektumok. A final mezőknél plusz garanciákat ad a memória modell: garantált inicializáltság. Ha létrejött az
objektum nem válik elérhetővé a referencia addig, amíg a mező nem elérhető. Módosíthatatlan objektumra jó példa a String osztály vagy ez a csomagoló osztály: unmodifiableMap a java Collections egy szolgáltatása olyan wrapper-t ad ami letiltja a módosító műveletek tudunk update-elni, nem tudunk újat felvenni törölni sem tudunk mindig mindenről másolatot készít, csak azt adja ki máshogy nem lehet a belső állapothoz hozzáférni Immutable objektumok tárolása azért hasznos, mert az alapból szinkronizál. Nem kell explicit lockot használnunk. 20. Szálra korlátozás, objektumra korlátozás. Egy szál lokális változójára mondhatjuk, hogy az adott szálra korlátozott. Minden szálnak saját stack-je van - lokális változókkal biztonságban vagyunk. Ha biztosítjuk, hogy egy elemre nem hivatkozhatnak a szálon kívül, akkor az az elem szálra korlátozott. Ha egy szál helyett egy objektumra korlátozzuk az adatot, akkor szálbiztossá tesszük az objektumot, vagyis lokkolunk. 21. Öröklődési problémák. Akkor tudunk kiterjeszteni szinkronizáltan, ha jól dokumentáltan, egyszerűen, szabványosan, specifikáltan szinkronizál a bázis osztály. Más esetben ugyanis törékeny lesz a megoldás, megváltozhat a bázis osztály szinkronizációja egy kód frissítéssel rejtett bomba Másik lehetőség, ha megvalósítjuk a List interface-t és mi oldjuk meg a szinkronizálást és hozzáadjuk a hiányolt műveletet az működhet, de csomó „boiler plate” kóddal jár és nem lesz „gusztusos” a megoldás. Ha final a belső mezőnk lekérdezéseknél nem kell szinkronizálni Nem könnyű szálbiztos osztályból származtatni. OOPElosztottság 22. Adatszerkezetek. Iterálás. Vannak eleve szinkronizált adatszerkezetek: Vector és Hashtable. CopyOnWriteArrayList az ArrayList szálbiztos változata, mely minden változtató művelet esetén egy teljesen új másolatot hoz létre a belső állapotról hatékony tud lenni, ha több az írás, mint az olvasás érzékeny erre ConcurrentHashMap finom lokkolást használ, nem a teljes adatszerkezeten hív műveleteket synchronized-al hatékonyabb párhuzamosítást tesz lehetővé nem érzékeny a teljesítménye a módosító/lekérdező műveletek arányára Arra is van lehetőség, hogy szinkronizációs burokba zártjuk a nem szinkronizáló adatszerkezeteket a Collections.synchronizedXxx műveletekkel. A synchronizedXxx csomagoló osztály nem biztosít szinkronizált iterátort. Ezt azt jelenti, hogy a szinkronizált objektumunk nem szálbiztos iterátort ad vissza. Nem is gondolnánk, milyen gyakran iterálunk: pl. final Set
set = new HashSet(); Random r = new Random(); for (int i = 0; i < 10; i++) add(r.nextInt()); System.out.println("DEBUG: added ten elements to " + set); A println a set default toString()-jét hívja, ami valószínűleg a HashSet iterátorát
használja majd a kiíratáshoz.. 23. Termelő-fogyasztó. Work-stealing. Könnyen megvalósítható BlockingQueue segítségével. Ha korlátozzuk a puffer méretét, azzal védekezhetünk a memóriarobbanás ellen. Ez tudja robosztussá tenni a programunkat. Azért hasznos ez a séma, mert logikai felbontását adja a rendszernek. Az alkalmazás egyes komponensei önmagukat hangolják; teljesítményben és időben összehangolódnak. Segítségével jól kihasználhatóak a magok. Viszonylag olcsón párhuzamosítunk, korlátozott pufferrel önszabályozó megoldást adva. Mik lehetnek a problémák? sima sort használva több termelő és több fogyasztó esetén ha lokkolni kell a teljes adatszerkezetet nagyon sok lesz a kontextus váltás. Nem lesz hatékony. Sok lesz a konfliktus blokkoló sor esetén a sor eleje és vége párhuzamosan hozzáférhető, ami nagyban javít a teljesítményen, de több termelő és több fogyasztó esetén még mindig nem az igazi. Köztük továbbra is fenn áll a verseny. az előbbi problémán segíthet, ha nem egy központi BlockingQueue-t használunk, hanem többet és párba állítjuk a termelő és fogyasztó szálakat. Ezzel viszont akkor van gond, ha nem egyenletes a termelés. Az egyik termelő lehet, hogy nagyon sokat termel és a fogyasztója nem bírja, míg más fogyasztók lehet, hogy hamar végeznek. Nem jól skálázódik a standard megoldás az, hogy minden fogyasztónak van egy „blocking deck” –je, ha azzal végez, másikat keres. 24. Szinkronizátorok (latch, barrier, semaphore). Future. Az alábbi „szinkronizátorok” a java.util.concurrent csomagban találhatók: CyclicBarrier Egy olyan segédeszköz, mellyel adott számú szál várakozik egymásra egy közös „sorompónál”. Akkor hasznos, ha fix számú dolgozó szálunk van, melyeknek néha várniuk kell egymásra. A „cyclic” a nevében arra utal, hogy újrahasznosítható a reset() metódus meghívásával. CountdownLatch Segítségével egy vagy több szállal várakozhatunk, amíg N másik szál végez a feladatával. Az N-el inicializáljuk a CountdownLatch-et majd az await metódussal blokkolhatunk szálakat. Ez a blokkolás addig tart, amíg N-szer meg nem hívjuk a countDown() metódust. Semaphore Maximum N engedélyt biztosít melyeket az acquire() metódussal kaphatnak meg a szálak. Ha elértük az N-t az újabb acquire-ok blokkolnak addig, amíg valamelyik szál meg nem hívja a release() metódust. A Future egy aszinkron számítás eredményét reprezentálja. Ez egy interface, mely metódusokat nyújt többek között annak ellenőrzésére, hogy a számítás befejeződött-e már, illetve, hogy megkapjuk az elkészült eredményt. A FutureTask egy egyszerű, „Runnable” megvalósítása az interface-nek. Az aszinkron akciók melyeket egy Future-el indítunk el happens-before relációban vannak a Future.get() metódushívással, mellyel az eredményeket kapjuk meg egy másik thread-ben. 25. Szálak és feladatok. Thread-pool. Executor. A szálak létrehozása általában költséges folyamat különösen, ha sok szálról van szó. A Thread objektumok nagy mennyiségű memóriát használnak és egy nagyméretű alkalmazásnál az állandó allokálása és de-allokálása ezeknek a szálaknak jelentős memóriamenedzselési overhead-et jelenthet. A thread-pool ezt a költséget tudja kiküszöbölni azzal, hogy pl. adott számú szálat allokál előre és ezeket biztosítja a feladatok futtatásához (fixed thread pool). Az Executor olyan osztály mely beküldött Runnable feladatokat képes futtatni. Általánosan
használt manuális Thread indítások helyett. pl. new Thread(new(RunnableTask1())).start(); new Thread(new(RunnableTask2())).start(); ... helyett Executor executor = anExecutor; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2()); ... 26. ExecutionService és CompletionService. Kötegelt feladatok végrehajtására használható az ExecutionService. Aszinkron feladatokat indít az elkészült munkák feldolgozására. A submit paranccsal küldhetünk be egy végrehajtandó feladatot, melyre egy Future-t kapunk vissza. A take segítségével a következő elkészült feladatot vehetjük ki a sorból szintén egy Future formájában. Tipikusan egy CompletionService egy Executor osztályra támaszkodik, vagyis ő csak az elkészült feladatokat tartalmazó belső sorért felelős. Ennek a módszernek egy megvalósítása az ExecutorCompletionService. 27.
Szálak leállítása, félbeszakítása. Szál: interrupt o java.lang.Thread o public void interrupt() o public boolean isInterrupted() o public static boolean interrupted() Feladat: cancel Executor: shutdown Időkorlátos végrehajtás
28. Feladat visszavonása (cancellation). CompletionService segítségével könnyen megvalósítható, mivel a take metódussal visszakapott Future-nek van cancel metódusa. 29. A wait-notify mechanizmus. Segítségükkel blokkolhatunk egy szálat addig, amíg egy feltétel nem teljesül. Minden objektumhoz wait-set Várakozás szignálig Zárolás szükséges wait előtt ellenőrizni a várakozási feltételt wait-et ciklusba szervezni notify és notifyAll Fontos, hogy ugyanarra az objektumra szinkronizáló blokkban hívjuk a wait és notify-t, hogy ne legyen úgy nevezett „missed signal”. 30. Holtpont, livelock, kiéheztetés. Holtpont: Két vagy több folyamat blokkolódik és egymásra vár, hogy két vagy több különböző erőforráshoz hozzáférjenek. Pl. a folyamat zárolta az X erőforrást és szeretné elérni az Y-t, míg a B folyamat zárolta az Y erőforrást és az X-et szeretné most zárolni. Livelock: A folyamtok nem blokkolódnak ugyan, de nem végeznek hasznos munkát. Kiéheztetés: Ütemezési probléma. Van olyan folyamat, amely nem jut erőforráshoz.
31. Prioritások. Priority inversion. Java-ban a szálaknak prioritása van. Alap állapotban, ha elindítunk egy szálat, annak a prioritása ugyanannyi lesz, mint az őt indító szál prioritása. A Thread osztály setPriority() és getPriority() metódusaival módosítható, illetve kérdezhető le ez a prioritás. A maximum kiosztható priorités 10, a minimum 1. A rendszer szálak 11-es prioritáson futnak. Priority inversion-ről akkor beszélünk, ha egy alacsonyabb prioritású folyamat implicit megelőz egy magasabb prioritású folyamatot. Példa: „Consider two tasks H and L, of high and low priority respectively, either of which can acquire exclusive use of a shared resource R. If H attempts to acquire R after L has acquired it, then H becomes unrunnable until L relinquishes the resource. The use of the shared exclusive-use resource when properly designed is such that L relinquishes R promptly enough that H's priority use is not hindered excessively. In spite of the good design of these two cooperating tasks, the surprising behavior, priority inversion, occurs when any third task M of medium priority becomes runnable during L's use of R. Once H becomes unrunnable, M is the highest priority runnable task, thus it runs and while it does L cannot relinquish R. So in this scenario, the medium priority task preempts the high priority task, resulting in a priority inversion.” 32. Programok hatékonyságának aspektusai: kiszolgálási idő, áteresztő képesség, válaszidő, skálázódás. 33. Profilozás szerepe. Amdahl és Gustaffson törvénye. A profilozás különböző JVM szintű paraméterek monitorozását jelent, mint pl. Method Execution, Thread Execution, Object Creation és Grabage Collection. Segítségével világosabb képet kaphatunk az alkalmazásunk végrehajtásáról és erőforrás kihasználásáról. Amdahl törvénye A több processzor használatával elérhető sebességnövekedés egy program esetén limitált azzal az idővel, ami a szekvenciális részének futtatásához szükséges. Tehát ha pl. egy programnak 20 óra kell, hogy egy szálon lefusson és ebből 1 órányi futás nem párhuzamosítható, csak a többi 19 óra (95%), akkor függetlenül attól hány processzorunk van, a minimális végrehajtási idő nem lehet kevesebb ennél a kritikus 1 óránál. Vagyis a sebességnövekedés max. 20x-os lehet. Gustafson törvénye A számítás hatékonyan párhuzamosítható tetszőlegesen nagy adathalmaz esetén. Látható, hogy ellentétben áll az Amdahl törvénnyel. P a processzorok száma, S a sebességnövekedés és párhuzamos folyamatnak.
a nem párhuzamosítható része bármely
34. Szinkronizáció költsége. Spin-lock és busy-waiting. Lock spliting. A szinkronizáció a párhuzamos végrehajtás ellen dolgozik, hiszen a szálakon időbeli rendezést definiál. Ebből következik, hogy minél többet szinkonizálunk szálaink között, annál kevesebb munka történik valóban párhuzamosan. busy-waiting spinlock
lock splitting
akkor beszélünk busy-waiting-ről, ha egy folyamat folyamatosan megvizsgál egy logikai állítást, hogy az igaz-e már egy fajta busy-waiting egy ciklusban (spin) folyamatosan várakozunk, amíg a lock-ot meg nem tudjuk szerezni nem hatékony független metódushalmazokat külön lock-okkal szinkronizálhatunk, így finomabb szinkronizációt érhetünk el
a metódusok definiálásakor nem használjuk a synchronized kulcsszót, de a törzsben az adott lock-ot használó synchronized blokkba kerül a kódunk 35.
Explicit lockok.
36. Író-olvasó szinkronizáció. ReadWriteLock interfész readLock writeLock Két lock-ot kezel, egyet az írásért és egyet explicite az olvasásért. Magasabb szintű konkurenciát tesz lehetővé, mint a kölcsönös kizárás. Kihasználja azt, hogy sok esetben míg csak egy szál írhatja az adatot, több szál olvashatja azt egyszerre. 37. CAS, nem blokkoló adatszerkezet. A CAS vagy compare-and-swap egy atomi utasítás többszálú környezetben, melyet szinkronizációs célból használhatunk. Mielőtt végrehajtanánk egy adott módosítást, összehasonlítjuk a memória értékét a kezünkben lévő régi értékkel és csak akkor változtatjuk a memória értékét a kívánt új értékre, ha egyezést találtunk. Mindezt egyetlen atomi utasításban tesszük.