1. gyakorlat – a párhuzamos programozás alapjai
A párhuzamos programozás lényege, hogy egy nagyobb feladatot kisebb részfeladatokra bontunk, és ezeket több végrehajtási egység próbálja egyidőben vagy időben átfedve végrehajtani. Java oldalon a legfontosabb szó itt a szál (thread).
Párhuzamosság alapok – mit kell biztosan érteni?
Parallelism
Ténylegesen egyszerre fut több művelet, tipikusan több magon vagy processzoron.
Concurrency
Több feladattal foglalkozunk, de nem feltétlenül egyszerre futnak; váltogatva is haladhatnak.
Miért jó a párhuzamosság?
- Gyorsíthatja a végrehajtást.
- Természetesebb lehet a problémaleírás.
- Nagy adatmennyiség feldolgozásánál kifejezetten hasznos.
Miért veszélyes?
- Versenyhelyzetet (race condition) okozhat.
- Holtpontot (deadlock) okozhat.
- Éhezést (starvation) okozhat.
- Nehéz fejben követni a futási sorrendet.
Thread osztály és Runnable interfész
Java-ban új szálat két klasszikus módon mutatnak be: Thread örökléssel vagy Runnable megvalósítással.
1. Thread osztályból származtatás
class ExampleThread extends Thread {
@Override
public void run() {
System.out.println("Hello World - Thread");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new ExampleThread();
t.start();
}
}
2. Runnable interfész megvalósítása
class ExampleRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello World - Runnable");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new ExampleRunnable());
t.start();
}
}
A legfontosabb különbség: start() vs run()
start()új szálat indít, és azon fut le arun().run()közvetlen hívása nem indít új szálat; sima metódushívás marad.
Thread t = new ExampleThread();
t.run(); // NEM új szál, csak egy sima metódushívás
t.start(); // valóban új szál indul
Lépésről lépésre: hogyan gondolkodj?
- Döntsd el, mi fusson párhuzamosan.
- A végrehajtandó logikát tedd a
run()metódusba. - Hozz létre egy
Threadobjektumot. - Indítsd el
start()-tal. - Ha meg kell várni a végét, használd a
join()-t.
Miért szokták jobbnak tartani a Runnable megoldást?
- Jobban szétválasztja a feladatot a szálkezeléstől.
- Mivel Java-ban csak egyszeres öröklés van, nem foglalja el az öröklést.
- Rugalmasabb, modernebb gondolkodásmód.
Hasznos Thread metódusok
class MethodThread extends Thread {
MethodThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("Current thread: " + Thread.currentThread());
System.out.println("Id: " + this.threadId());
System.out.println("Name: " + this.getName());
System.out.println("Alive: " + this.isAlive());
System.out.println("Daemon: " + this.isDaemon());
System.out.println("Priority: " + this.getPriority());
}
}
sleep(ms)
Ideiglenesen megállítja az aktuális szál futását. Gyakran dob InterruptedException-t.
join()
Egy másik szál befejeződésére vár. Nagyon gyakori ZH-s eszköz.
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
});
t.start();
t.join(); // a main thread megvárja t befejeződését
System.out.println("A szál már lefutott.");
join()-nal,
ezért a main túl korán ér véget vagy túl korán ír ki eredményt.
Szálállapotok
A Java Thread.State szempontból ezeket érdemes tudni:
- NEW – létrehoztad a szálat, de még nem indítottad el.
- RUNNABLE – fut vagy futásra kész.
- BLOCKED – zárra vár, nem tud belépni a kritikus szakaszba.
- WAITING – valamilyen jelzésre vár, pl.
wait(),join(). - TIMED_WAITING – időzített várakozásban van, pl.
sleep(). - TERMINATED – már befejeződött.
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // valószínűleg RUNNABLE vagy TIMED_WAITING
BLOCKED vs WAITING – ezt gyakran keverik
- BLOCKED: zárra vár.
- WAITING: feltétel teljesülésére / értesítésre vár.
Párhuzamosság teljesítményjavításra
Klasszikus ötlet: egy nagy bemenetet több részre bontasz, minden szál a saját részét dolgozza fel, majd a részeredményeket összegzed.
Példa: szavak gyakoriságának számolása
- Beolvasod a teljes szöveget.
- Felosztod több részre.
- Minden szál a saját részén számol.
- Minden szál egy lokális
HashMap-be gyűjt. - A végén a lokális térképeket összefésülöd.
class CounterTask implements Runnable {
private final List<String> part;
private final Map<String, Integer> localMap = new HashMap<>();
public CounterTask(List<String> part) {
this.part = part;
}
@Override
public void run() {
for (String word : part) {
localMap.put(word, localMap.getOrDefault(word, 0) + 1);
}
}
public Map<String, Integer> getLocalMap() {
return localMap;
}
}
2. gyakorlat – kölcsönös kizárás, monitor, wait/notify
Itt jönnek az igazi veszélyek: ugyanazt a közös adatot több szál egyszerre olvassa és írja. Emiatt kell a szinkronizáció.
Kölcsönös kizárás és race condition
Ha több szál ugyanahhoz az erőforráshoz fér hozzá, akkor biztosítani kell, hogy egyszerre csak a megfelelő számú szál használja azt. Ez a kölcsönös kizárás.
class Counter {
private int value = 0;
public void increment() {
value++; // ez NEM atomi művelet
}
public int getValue() {
return value;
}
}
A value++ valójában több lépésből áll: beolvasás, növelés, visszaírás.
Ha két szál egyszerre végzi, elveszhet egy növelés.
Javítás synchronized-del
class Counter {
private int value = 0;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}
Monitor Java-ban
A monitor olyan mechanizmus, amely egy közös erőforráshoz ellenőrzött hozzáférést ad.
Java-ban a synchronized kulcsszóval használjuk az objektum implicit zárját.
Synchronized blokk
public class SynchronizedBlock {
private final Object lock = new Object();
public void perform() {
synchronized (lock) {
// kritikus szakasz
}
}
}
Synchronized metódus
public class SynchronizedMethod {
public synchronized void perform() {
// kritikus szakasz
}
}
Mikor melyiket?
- Synchronized metódus: ha az egész metódus kritikus szakasz.
- Synchronized blokk: ha csak egy rész kritikus, vagy külön zárobjektum kell.
synchronized esetén a zár a this,
statikus synchronized metódus esetén pedig az osztályobjektum.
Holtpont (deadlock)
Deadlock akkor alakul ki, amikor két vagy több szál egymásra vár olyan módon, hogy egyik sem tud továbbhaladni.
class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void methodA() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("A");
}
}
}
public void methodB() {
synchronized (lockB) {
synchronized (lockA) {
System.out.println("B");
}
}
}
}
Mi a baj?
- Az egyik szál megszerzi
lockA-t, majd várlockB-re. - A másik megszerzi
lockB-t, majd várlockA-ra. - Mindkettő áll.
Hogyan védekezz ellene?
- Mindig ugyanabban a sorrendben szerezd meg a zárakat.
- Minimalizáld a több zár egyidejű használatát.
- Használj átgondolt zárolási stratégiát.
Kapcsolódó fogalom: starvation (éhezés), amikor egy szál nem feltétlenül holtpont miatt, hanem igazságtalan ütemezés miatt szinte sosem jut erőforráshoz.
A szálak várakoztatása és felébresztése: wait(), notify(), notifyAll()
Ha egy szál belépett a monitorba, de a folytatáshoz szükséges feltétel még nem teljesül, akkor nem blokkolnia kell a többieket, hanem várakoznia.
wait(): a szál várakozó állapotba kerül, és elengedi a monitor zárját.notify(): egy várakozó szálat felébreszt.notifyAll(): az összes várakozó szálat felébreszti.
Nagyon fontos szabályok
- Ezeket csak monitoron belül, tipikusan
synchronizedblokkban vagy metódusban szabad hívni. - A
wait()után a szál csak akkor tud továbbmenni, ha újra megszerzi a zárat. - A feltételt while-lal ellenőrizzük, nem if-fel.
synchronized (lock) {
while (!feltetel) {
lock.wait();
}
// itt már teljesül a feltétel
}
Termelő–fogyasztó probléma monitorral
Van egy közös puffer. A termelő beletesz elemeket, a fogyasztó kiveszi. Ha tele a puffer, a termelő vár. Ha üres, a fogyasztó vár.
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public Buffer(int capacity) {
this.capacity = capacity;
}
public synchronized void put(int value) throws InterruptedException {
while (queue.size() == capacity) {
wait();
}
queue.add(value);
notifyAll();
}
public synchronized int take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int value = queue.remove();
notifyAll();
return value;
}
}
Mit figyelj meg benne?
- Mindkét metódus
synchronized. - Várakozásnál
whilevan. - Állapotváltozás után
notifyAll()van. - A puffer a közös kritikus erőforrás.
3. gyakorlat – Lock, Condition, Semaphore
Ugyanazokat a problémákat oldjuk meg, mint korábban, csak rugalmasabb és explicitebb eszközökkel.
Lock és Condition interfészek
A monitoros megközelítés néha túl merev. A Lock és a Condition több kontrollt ad.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Example {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void doWork() throws InterruptedException {
lock.lock();
try {
while (!feltetel()) {
condition.await();
}
// kritikus szakasz
condition.signalAll();
} finally {
lock.unlock();
}
}
private boolean feltetel() {
return true;
}
}
Megfeleltetések
synchronized↔lock.lock()éslock.unlock()wait()↔condition.await()notify()↔condition.signal()notifyAll()↔condition.signalAll()
lock.lock() van, akkor szinte biztosan kell try/finally,
hogy az unlock() biztosan lefusson.
Termelő–fogyasztó Lock + Condition megoldással
Itt külön feltételváltozókat is használhatsz, például egyet a "nem üres" és egyet a "nem tele" állapotra. Ez tisztább és hatékonyabb lehet.
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public Buffer(int capacity) {
this.capacity = capacity;
}
public void put(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.add(value);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.remove();
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}
Miért kényelmesebb ez?
- Nem mindenkit kell felébreszteni mindig.
- Külön várakozási sorok lehetnek külön feltételekre.
- Olvashatóbb sok bonyolult feladatnál.
Olvasók–írók probléma
A cél: több olvasó olvashasson egyszerre, de író csak egyedül írhasson. Írás közben ne olvasson senki, olvasás közben pedig író ne módosítson.
Alapelv
- Olvasók egymással kompatibilisek.
- Író senkivel sem kompatibilis.
- A megoldás lehet olvasópreferáló vagy írópreferáló.
class Book {
private final Lock lock = new ReentrantLock();
private int readers = 0;
private boolean writing = false;
public void read() {
lock.lock();
try {
while (writing) {
// await kellene egy teljes megoldásban
}
readers++;
} finally {
lock.unlock();
}
try {
System.out.println("Olvasás...");
} finally {
lock.lock();
try {
readers--;
} finally {
lock.unlock();
}
}
}
}
ZH-n nem mindig a teljes kész kód a lényeg, hanem hogy meg tudd indokolni az együttfutási szabályokat.
Semaphore osztály
A szemafor egy számlálóalapú szinkronizációs eszköz. A benne tárolt érték azt mutatja, hány "belépés" vagy hozzáférés engedélyezett még.
Bináris szemafor
Értéke tipikusan 0 vagy 1. Gyakorlatilag kölcsönös kizárásra használható.
Számláló szemafor
Több hozzáférést enged egyszerre, például 3 vagy 10 szálnak.
import java.util.concurrent.Semaphore;
class SharedResource {
private final Semaphore semaphore = new Semaphore(1);
public void use() throws InterruptedException {
semaphore.acquire();
try {
System.out.println("Kritikus szakasz");
} finally {
semaphore.release();
}
}
}
Mit jelent a két fő művelet?
acquire()– engedélyt kér, ha nincs, vár.release()– visszaad egy engedélyt.
Étkező filozófusok probléma
Klasszikus szinkronizációs feladat. Minden filozófusnak két villa kell az evéshez, de a villák közösek. Ha mindenki ugyanabban a rossz sorrendben vesz fel villát, deadlock lehet.
A probléma lényege
- Mindenki verseng a szomszédos erőforrásokért.
- A rossz zárolási sorrend holtpontot okozhat.
- A cél: ne legyen se deadlock, se starvation.
Tipikus megoldási ötletek
- Egy filozófus fordított sorrendben vesz fel villát.
- Legfeljebb 4 filozófus próbálkozhat egyszerre.
- Sorszámozott villák, mindig kisebb sorszámút vesszük fel előbb.
Parkolóház számláló szemaforral
Ez tökéletes számláló szemafor példa. Ha a parkolóban 10 hely van, akkor a szemafor kezdőértéke 10.
Minden autó belépéskor acquire()-ol, kilépéskor release()-el.
class ParkingLot {
private final Semaphore places;
public ParkingLot(int capacity) {
this.places = new Semaphore(capacity);
}
public void enter() throws InterruptedException {
places.acquire();
System.out.println("Autó beállt");
}
public void leave() {
System.out.println("Autó kiállt");
places.release();
}
}
Miért jó példa?
- Az erőforrások száma jól modellezhető egész számmal.
- Nem csak 1 szál mehet be, hanem korlátozott számú.
- Nagyon könnyű fejben elképzelni.
ZH gondolkodási menet – hogyan állj neki egy gyakorlati feladatnak?
- Azonosítsd a közös erőforrást. Mi az, amit több szál használ?
- Azonosítsd a veszélyt. Race condition? Várakozás? Deadlock? Korlátozott kapacitás?
- Válassz eszközt.
- Egyszerű kölcsönös kizárás:
synchronizedvagy bináris szemafor - Feltételes várakozás:
wait/notifyvagyCondition - Korlátozott számú belépés: számláló
Semaphore
- Egyszerű kölcsönös kizárás:
- Írd meg a kritikus szakaszt.
- Várakozási feltétel esetén while-t használj.
- Locknál mindig legyen finally + unlock.
- A végén gondold át: lehet-e holtpont, éhezés, elvesző értesítés, túl korai kiírás?
Fogalomtár
- Thread
- Végrehajtási szál egy folyamaton belül.
- Runnable
- Interfész, amelynek
run()metódusa tartalmazza a végrehajtandó feladatot. - start()
- Új szálat indít.
- run()
- A szál feladatát leíró metódus; önmagában meghívva nem indít új szálat.
- join()
- Megvárja egy másik szál befejeződését.
- sleep()
- Időzített várakozásba teszi az aktuális szálat.
- Mutual exclusion / kölcsönös kizárás
- Egyszerre csak a megfelelő számú szál férhet hozzá a kritikus erőforráshoz.
- Race condition
- A futási sorrend befolyásolja a helyes eredményt; tipikusan hibás viselkedéshez vezet.
- Critical section / kritikus szakasz
- A kód azon része, ahol közös erőforrást használunk.
- Monitor
- Szinkronizációs mechanizmus, amely kontrollálja a közös erőforráshoz való hozzáférést.
- synchronized
- Java kulcsszó monitoralapú kölcsönös kizáráshoz.
- wait()
- Várakozó állapotba teszi a szálat és elengedi a monitor zárját.
- notify()
- Egy várakozó szálat ébreszt.
- notifyAll()
- Az összes várakozó szálat ébreszti.
- Lock
- Expliciten kezelt zár, például
ReentrantLock. - Condition
- Feltételváltozó Lock mellett; külön várakozási sorokat tesz lehetővé.
- await()
- Condition melletti várakozás.
- signal()
- Egy várakozó szálat ébreszt a Condition sorából.
- signalAll()
- Az összes várakozó szálat ébreszti a Condition sorából.
- Semaphore
- Engedélyek számával dolgozó szinkronizációs eszköz.
- Binary semaphore
- 0/1 jellegű szemafor, kölcsönös kizárásra is használható.
- Counting semaphore
- Több egyidejű hozzáférést engedő szemafor.
- Deadlock
- Egymásra váró szálak miatt a rendszer nem halad tovább.
- Starvation
- Egy szál tartósan nem jut erőforráshoz.
- BLOCKED
- A szál zárra vár.
- WAITING
- A szál értesítésre vagy feltételre vár.
- TIMED_WAITING
- Időzített várakozásban lévő szál.
Lehetséges feleletválasztós kérdések
1. Melyik állítás igaz?
A) A run() mindig új szálat indít.
B) A start() új szálat indít.
C) A join() lezárja a szálat.
D) A sleep() felébreszt egy másik szálat.
Megoldás: B
2. Mire való a synchronized?
A) Új szál létrehozására
B) Kölcsönös kizárás biztosítására
C) Szál megszakítására
D) Memóriafoglalásra
Megoldás: B
3. Melyik állapot jellemző Thread.sleep() után?
A) BLOCKED
B) RUNNABLE
C) TIMED_WAITING
D) TERMINATED
Megoldás: C
4. Melyik a helyes párosítás?
A) wait() ↔ Lock
B) await() ↔ Condition
C) notifyAll() ↔ Semaphore
D) release() ↔ Thread
Megoldás: B
5. Melyik a deadlock tipikus oka?
A) Túl sok print utasítás
B) Egymásra váró szálak és zárak
C) Túl gyors CPU
D) Túl kevés osztály
Megoldás: B
6. Miért szokás while-t használni wait() körül?
A) Mert az if tiltott Java-ban
B) Mert felébredés után újra ellenőrizni kell a feltételt
C) Mert a while gyorsabb
D) Mert csak így működik a notify()
Megoldás: B
7. Mire jó a számláló szemafor?
A) Pontosan egy szál belépésére
B) Több, de korlátozott számú szál egyidejű belépésére
C) Szálak törlésére
D) Kód újrafordítására
Megoldás: B
8. Melyik állítás hamis?
A) A join() várakozást okozhat.
B) A Semaphore.acquire() várhat engedélyre.
C) A notifyAll() automatikusan felszabadítja az összes zárat.
D) A Lock használatánál kell unlock().
Megoldás: C