Thread alapok
Thread vs Runnable, életciklus és daemon szálak
Thread alapok
Definíció
A Java thread egy önálló végrehajtási útvonal, amelyet a JVM kezel, és a legtöbb esetben egy operációs rendszer szálra képez le. A Thread objektum csak a Java oldali fogantyú; a tényleges viselkedést az ütemező, a Java Memory Model, a blokkoló rendszerhívások, a safepointok és a megosztott memória állapota határozza meg. Éles rendszerekben ezért a thread alapok nem merülnek ki a start() hívásban: érteni kell az életciklust, a megszakítást, a hibakezelést, a daemon viselkedést és azt is, mennyibe kerül egy feladatot platform szálhoz kötni.
A concurrency arról szól, hogy a program több egysége egymástól függetlenül tudjon haladni. A parallelism pedig arról, hogy ezek fizikailag egyszerre fussanak több magon. Egy szolgáltatás lehet erősen konkurens akkor is, ha kevéssé párhuzamos, például mert a szálak nagy része I/O-ra, lockokra vagy backpressure-re vár. Senior szinten a thread primitívek használata mindig kapacitás-, diagnosztikai és leállítási kérdés is.
Alapfogalmak
Az első fontos különbség a Thread és a Runnable között van. A Thread örökítése összeköti a feladatot és a futtatási mechanizmust, míg a Runnable szétválasztja a „mit kell csinálni” és a „hogyan fusson” kérdését. Emiatt a valós rendszerekben jellemzően Runnable vagy Callable feladatokat adunk executornak, nem pedig Thread-et örökítünk. A Callable akkor fontos, ha eredményt vagy kivételt is vissza kell adni a háttérmunkából.
A Java jól definiált thread életciklust mutat: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. A RUNNABLE kicsit félrevezető: HotSpot alatt ez azt jelenti, hogy a szál futásra kész, nem feltétlenül azt, hogy éppen CPU-n fut. Lehet, hogy az OS ütemezőre vagy natív I/O-ra vár. A BLOCKED többnyire azt jelzi, hogy a szál egy synchronized monitorra vár. A WAITING és TIMED_WAITING tipikusan Object.wait(), Thread.sleep(), LockSupport.park() vagy Thread.join() miatt jelenik meg.
A daemon thread olyan háttérszál, amely nem tartja életben a JVM-et. Ha már csak daemon szálak maradnak, a JVM bármikor kiléphet, még akkor is, ha ezek éppen logot írnak, metrikát flush-ölnek vagy állapotot frissítenek. Ezért daemon szál háttérsegédekhez jó, de olyan feladathoz veszélyes, amelynek garantáltan be kell fejeződnie. A shutdown hook sem helyettesíti a korrekt lifecycle-kezelést; az utolsó mentőöv, nem pedig üzleti workflow.
Az interrupt a Java együttműködő megszakítási mechanizmusa. Az interrupt() nem állítja le erőszakkal a szálat. Beállít egy interrupt flaget, és bizonyos blokkoló hívások InterruptedException-t dobnak. A jó kód vagy továbbadja ezt, vagy visszaállítja az interrupt státuszt Thread.currentThread().interrupt() hívással. Az interrupt elnyelése miatt a poolok nem állnak le rendesen, a cancellation pedig megbízhatatlanná válik.
Gyakorlati használat
Nyers threadet akkor érdemes használni, ha alapfogalmat tanítasz, alacsony szintű API-val integrálsz, vagy egy kis, jól körülhatárolt háttérmunkásnak egyértelmű tulajdonosa van. Még ekkor is adj beszédes nevet a szálnak, állíts be UncaughtExceptionHandler-t, és tedd egyértelművé a leállítási viselkedést. A név nélküli thread később thread dumpban és profilerben diagnosztikai adóvá válik.
Úgy tervezd a feladatokat, hogy együttműködően meg tudjanak állni. Hosszú ciklusokban rendszeresen ellenőrizd a Thread.currentThread().isInterrupted() értékét. Blokkoló kódban használj interruptbarát API-kat. Ha cleanup szükséges, kapd el az InterruptedException-t, állítsd vissza a flaget, zárd le az erőforrásokat, majd gyorsan térj vissza. Az „ignoráld és menj tovább” ritkán helyes stratégia.
A ThreadLocal külön odafigyelést igényel. Hasznos lehet request correlationre vagy threadenkénti cache-re, de poolozott szálaknál az érték túléli a logikai kérést, ha nem törlöd explicit módon. Ez classloader leakhez, nehezen reprodukálható tesztszennyezéshez és memória-visszatartáshoz vezethet. Úgy kezeld, mint manuálisan menedzselt erőforrást: szűk scope-ban állítsd be, és finally blokkban töröld.
Kód példák
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("processing batch");
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("worker stopped cleanly");
}, "inventory-sync-1");
worker.setUncaughtExceptionHandler((t, ex) ->
System.err.println(t.getName() + " failed: " + ex.getMessage()));
worker.start();
// később
worker.interrupt();
worker.join();
A lényeg nem a lambda rövidsége, hanem az interrupt policy: a ciklus figyeli a flaget, a sleep megszakítása visszaáll a flagbe, a join() pedig megvárja a rendezett leállást.
class HeartbeatService {
private final AtomicBoolean running = new AtomicBoolean();
private Thread thread;
void start() {
if (!running.compareAndSet(false, true)) {
return;
}
thread = new Thread(this::runLoop, "heartbeat-daemon");
thread.setDaemon(true);
thread.start();
}
private void runLoop() {
while (running.get() && !Thread.currentThread().isInterrupted()) {
try {
sendHeartbeat();
Thread.sleep(1_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
void stop() throws InterruptedException {
running.set(false);
if (thread != null) {
thread.interrupt();
thread.join(2_000);
}
}
private void sendHeartbeat() {}
}
Ez a példa daemon segédszálat mutat explicit leállítási logikával. A daemon flag segít, hogy ne tartsa életben a JVM-et, de a szolgáltatás ettől még ad stop() metódust, mert a graceful shutdownot nem szabad kizárólag a daemon szemantikára bízni.
Trade-offok
A nyers threadek nagy kontrollt és tiszta mentális modellt adnak, de rosszul skálázódnak általános munkavégzési egységként. Minden platform thread stack memóriát fogyaszt, növeli az ütemezési költséget és context switch overheaddel jár. A „thread per request” megközelítés kis terhelésnél működhet, de csúcsterhelésnél inkább memória- és latency-problémákhoz vezet, nem lineáris lassuláshoz.
A daemon thread csökkenti a leállítási súrlódást best-effort feladatoknál, viszont növeli a csendes adatvesztés kockázatát, ha kritikus állapotot kezel. Az interrupt könnyű és jól kombinálható megszakítási jel, de csak akkor, ha a saját és a használt kód tiszteletben tartja. A ThreadLocal kényelmes, de rejtett kapcsolatot teremt a logikai kérés és a fizikai szál között.
A gyakorlati szabály: a nyers thread legyen tulajdonosi boundary, ne általános task-dispatch mechanizmus. Amint admission control, pooling, eredménykezelés vagy strukturált shutdown kell, lépj tovább a concurrency API-k felé.
Gyakori hibák
A klasszikus hiba a run() meghívása start() helyett: ekkor a kód szinkron módon, a hívó szálon fut, és nem jön létre konkurencia. Ugyanilyen gyakori félreértés, hogy a sleep() elengedné a lockot; nem engedi. Ha egy thread synchronized blokkban alszik, továbbra is tartja a monitort és blokkolhat másokat.
Az interrupt kezelés az a terület, ahol sok egyébként jó kódbázis elbukik. Ha elkapod az InterruptedException-t és csak logolsz, akkor törlöd az interrupt státuszt, és a pool shutdown könnyen beragad. Hasonlóan veszélyes a Thread.interrupted() használata, ha valójában isInterrupted() kellett volna, mert a statikus metódus ki is üríti a flaget.
Tipikus production anti-pattern az is, amikor valaki daemonra állít egy threadet csak azért, mert a JVM „nem akar kilépni”. Ilyenkor a valódi hiba szinte mindig a menedzseletlen életciklus. A daemon flag elfedi a tünetet, de gyengíti a completion garanciát. Kerüld a kontrollálatlan thread létrehozást ciklusokban is; terhelési csúcsnál a rendszer több időt tölt ütemezéssel, mint hasznos munkával.
Senior szintű meglátások
JVM szinten a thread állapot nemcsak a Java API-ban látszik, hanem diagnosztikai eszközökben is, például jstack, Java Flight Recorder vagy async profiler kimenetében. Senior szinten fontos megtanulni a tüneteket összekötni a dumpokkal: sok BLOCKED thread monitor contentionre utal, sok TIMED_WAITING backoffra vagy sleep-alapú pollingra, sok magas CPU-t fogyasztó RUNNABLE thread pedig spin loopra vagy natív hívásra.
A thread életciklus összefügg a safepointokkal és a stop-the-world eseményekkel is. Egy thread kódszinten ártalmatlannak tűnhet, mégis ronthatja a GC vagy a safepoint belépés viselkedését, ha problémás natív kódot futtat vagy nagy, szoros ciklusban dolgozik. Ha megérted, hogy minden Java thread a teljes VM koordinációs rendszer része, akkor jobban érthetővé válik, miért nem „ingyenes” még egy szál hozzáadása.
A modern Java virtuális szálakat is ad, de ettől az alapelvek nem tűnnek el. Az interrupt, az együttműködő leállítás, a thread naming, az uncaught exception policy és a safe publication továbbra is fontos. A különbség az, hogy sok workloadnál olcsóbbá válik a blokkolás, miközben pinning problémák továbbra is előfordulhatnak synchronized vagy natív műveletek körül. Aki érti a platform thread alapjait, gyorsabban és biztonságosabban tud Loomra váltani.
Szószedet
- Thread: JVM által kezelt végrehajtási útvonal saját veremmel.
- Runnable: Visszatérési érték nélküli feladatszerződés.
- Callable: Eredményt adó vagy kivételt dobó feladatszerződés.
- Daemon thread: Háttérszál, amely nem tartja életben a JVM-et.
- Interrupt: Együttműködő megszakítási jel thread állapot formájában.
- ThreadLocal: Threadenkénti tároló, amelyet poolozott környezetben explicit takarítani kell.
- Thread dump: Diagnosztikai pillanatkép a szálakról és stackjeikről.
- Safepoint: JVM koordinációs pont GC-hez és egyéb VM műveletekhez.
Gyorsreferencia
- Inkább
Runnable/Callable, mintThreadöröklés. - Konkurens futtatáshoz
start()kell, nemrun(). - Adj üzleti szerep alapú threadnevet:
order-writer-1. - Hosszú életű szálaknál kezeld az uncaught kivételeket.
- Az interruptot kezeld cancellation kérésként; add tovább vagy állítsd vissza.
- A
sleep()megállítja a végrehajtást, de nem engedi el a lockot. - A
join()megvárja egy másik thread befejezését, és megszakítható. - Daemon threadet csak best-effort háttérmunkára használj.
ThreadLocalértéketfinallyblokkban töröld, ha a thread újrahasznosul.- Általános task dispatchre inkább executorokat használj.
🎮 Játékok
10 kérdés