Interview questions 2
1 – Mi az a deadlock, és hogyan lehet elkerülni?
Deadlock: Olyan helyzet, amikor két vagy több szál egymásra vár – mindkettő tart egy lockot, és a másik szál lockjára vár, így egyik sem tud tovább haladni.
Klasszikus példa: - Szál A megszerezte az 1-es lockot, és várja a 2-eset. - Szál B megszerezte a 2-es lockot, és várja az 1-eset. - Mindkettő vár örökre.
4 feltétel kell a deadlockhoz (Coffman-feltételek): 1. Mutual exclusion – az erőforrást egyszerre csak egy szál használhatja. 2. Hold and wait – egy szál tart egy lockot, és újabbra vár. 3. No preemption – a lockot nem lehet erőszakkal elvenni, csak a tartó szál adhatja fel. 4. Circular wait – körkörös várakozási lánc alakul ki.
Elkerülési stratégiák:
- Lock ordering: Mindig ugyanabban a sorrendben szerezzük meg a lockokat. Ha minden szál előbb az 1-est, majd a 2-eset kéri, körkörösen várakozás nem alakulhat ki.
- tryLock() időkorláttal: A
ReentrantLock.tryLock(timeout)megpróbálja megszerezni a lockot, de ha nem sikerül időn belül, visszalép – így nem vár örökre. - Lock granularitás csökkentése: Minél kevesebb ideig tartunk lockot és minél kisebb kódrészt védünk vele, annál kisebb az esély deadlockra. Csak a kritikus szekciót zárjuk le, ne az egész metódust.
- Immutable objektumok használata: Ha az állapot nem változik, nincs szükség lockra.
2 – Mi az a JVM memória modell, és mik a főbb területek?
A JVM (Java Virtual Machine) a Java program futtatásáért felelős virtuális gép. A memóriát több területre osztja fel, mindegyiknek más a feladata.
Heap: - Az összes objektum és tömb alapértelmezetten itt jön létre. - Az objektumok mezői itt tárolódnak az objektummal együtt – beleértve a primitív mezőket és a field referenciákat (más objektumokra mutató mezők) is. - A Garbage Collector ezt kezeli. - Két fő része van: - Young Generation – újonnan létrehozott objektumok kerülnek ide. Ha túlélik a GC ciklust, átkerülnek az Old Generation-be. - Old Generation (Tenured) – hosszabb életű objektumok helye.
Stack: - Szálanként külön stack létezik. - A metódushívások és lokális változók kerülnek ide. - Lokális primitív változók értéke közvetlenül itt tárolódik. - Lokális referencia változók szintén itt vannak – de maga az objektum amit mutatnak, alapértelmezetten a Heap-en él. - LIFO (Last In First Out) struktúra. - Ha túl mély a rekurzió → StackOverflowError.
Metaspace: - Az osztályok metaadatait (class metadata – osztályszerkezet, metódusok leírása) tárolja. - Statikus primitív mezők értéke itt tárolódik. - Statikus referencia mezők szintén itt vannak – de maga az objektum amit mutatnak, a Heap-en él. - Natív memóriát használ, dinamikusan nő.
Code Cache: - A JIT (Just-In-Time) compiler által lefordított natív gépi kód kerül ide. - JIT compiler: A gyakran futó bytecode-ot natív gépi kódra fordítja futás közben, hogy gyorsabb legyen a végrehajtás.
JIT optimalizáció – Escape Analysis: A JIT elemzés során megállapíthatja, hogy egy objektum nem „szökik ki" a metódusból (más szálak nem férnek hozzá, metóduson kívül nem érhető el). Ekkor a Heap helyett a Stack-re allokálja (Stack Allocation). Előnyei: gyorsabb allokáció, nincs GC nyomás, a metódus végén automatikusan felszabadul. Tehát a „minden objektum a Heap-en van" szabály technikailag nem 100% igaz.
Összefoglalva:
| Terület | Mit tárol? | Szálanként? |
|---|---|---|
| Heap | Objektumok, tömbök, primitív mezők, field referenciák | Nem, közös |
| Stack | Metódushívások, lokális primitívek, lokális referenciák | Igen |
| Metaspace | Osztály metaadatok, statikus mezők | Nem, közös |
| Code Cache | JIT által fordított kód | Nem, közös |
3 – Mi az a Garbage Collection, és hogyan működik a JVM-ben?
A Garbage Collection (GC) egy automatikus memóriakezelési mechanizmus, amely felszabadítja a már nem elérhető (unreachable) objektumok által foglalt heap memóriát.
Mikor számít egy objektum unreachable-nek? Ha egyetlen élő szálból sem érhető el semmiféle referencia láncon keresztül. A GC nem a referenciák számát számolja (mint a reference counting), hanem elérhetőségi elemzést végez a GC root-okból kiindulva.
GC root-ok: Azok a kiindulópontok, amelyektől az elérhetőség vizsgálata indul: - Stack-en lévő lokális változók - Statikus mezők - Aktív szálak
Hogyan működik – Generational GC: A JVM azt az elvet használja, hogy a legtöbb objektum rövid életű. Ezért a heap generációkra van osztva:
-
Young Generation – Minor GC:
- Új objektumok az Eden területen jönnek létre.
- Ha az Eden megtelik, Minor GC fut.
- A túlélők a Survivor területekre (S0, S1) kerülnek.
- Aki elég sok GC ciklust túlélt → átkerül az Old Generation-be (ezt hívják promotion-nek).
-
Old Generation – Major GC:
- A hosszú életű objektumok helye.
- Ritkábban fut, de tovább tart.
-
Full GC:
- Az egész heap-et érinti (Young + Old).
- Teljesítmény szempontból a legköltségesebb, lehetőleg kerülendő.
Stop-the-world esemény: A legtöbb GC fázis megállítja az összes alkalmazás szálat (stop-the-world – STW), amíg a GC dolgozik. A modern GC algoritmusok ezt minimalizálják.
Főbb GC algoritmusok Java-ban: - G1GC (Garbage First) – Java 9 óta alapértelmezett. A heap-et kis régiókra osztja, előre jelezhetővé teszi a szüneteket. - ZGC – Alacsony latenciájú GC, a stop-the-world szünetek milliszekundum alatt maradnak. Nagy heap-eknél ajánlott. - Parallel GC – Többszálú, áteresztőképességre optimalizált, de hosszabb STW szünetekkel.
Manuális GC hívás: A System.gc() csak javaslatot tesz a JVM-nek, nem garantált hogy lefut.
Memory leak: Akkor fordul elő, ha egy objektum már nem használt, de valamilyen referencia még mindig él rá – ezért a GC nem tudja felszabadítani. Java-ban ritkább mint C++-ban, de előfordulhat: - Statikus kollekcióba rakott objektumok, amiket soha nem távolítunk el. - Nem lezárt erőforrások (stream, connection). - Listener/callback regisztrációk, amiket soha nem törlünk.
4 – Mi az az ExecutorService és a Thread Pool?
Mi a probléma a sima Thread-ekkel? Ha minden feladathoz új Thread-et hozunk létre, az költséges – a szálak létrehozása és megszüntetése erőforrásigényes, és túl sok szál esetén a rendszer lelassul a context switching (szálak közötti váltás) miatt.
Thread Pool: Előre létrehozott szálak készlete, amelyek újra felhasználhatók. A feladatok egy sorba (queue) kerülnek, és a szabad szálak veszik ki és hajtják végre őket.
ExecutorService: A Java java.util.concurrent csomagjának absztrakciója a thread pool kezelésére. Feladatokat lehet beküldeni (submit(), execute()), és az eredményt Future-ön keresztül lehet lekérni.
Főbb factory metódusok (Executors osztály): - newFixedThreadPool(n) – fix számú szál, a többi feladat várakozik. - newCachedThreadPool() – szükség szerint hoz létre szálakat, tétlen szálakat 60mp után megszüntet. Rövid, sok feladatnál hasznos. - newSingleThreadExecutor() – egyetlen szál, sorrendi végrehajtás garantált. - newScheduledThreadPool(n) – időzített vagy ismétlődő feladatokhoz. - newVirtualThreadPerTaskExecutor() – Java 21+, minden feladathoz új Virtual Thread-et hoz létre. Nem igényel pool-t, I/O-intenzív feladatoknál ajánlott.
Future<T>: A submit() visszatér egy Future-rel, amellyel lekérdezhető az eredmény (get()) vagy megszakítható a feladat (cancel()). A get() blokkoló hívás – addig vár, amíg az eredmény elkészül. Hátránya, hogy nem lehet láncolni és kivételkezelése körülményes.
CompletableFuture<T> (Java 8+): A Future továbbfejlesztett változata, amely aszinkron, láncolható műveleteket tesz lehetővé – blokkolás nélkül. - thenApply() – az eredményt transzformálja, ha kész (map-szerű). - thenAccept() – az eredményt felhasználja, de nem ad vissza értéket. - thenCompose() – egy másik CompletableFuture-t lánc be (flatMap-szerű). - thenCombine() – két független CompletableFuture eredményét kombinálja. - exceptionally() – hibakezelés: ha kivétel dobódik, egy alapértelmezett értéket ad vissza. - allOf() – megvárja, amíg az összes megadott CompletableFuture elkészül. - anyOf() – az első elkészült eredményét adja vissza.
Ha nem adunk meg saját Executor-t, a CompletableFuture a közös ForkJoinPool.commonPool()-t használja.
Leállítás: - shutdown() – nem fogad új feladatot, de a már beküldötteket befejezi. - shutdownNow() – megpróbálja azonnal leállítani a futó szálakat is.
Mikor mekkora pool? - CPU-intenzív feladatok → pool mérete ≈ CPU magok száma. - I/O-intenzív feladatok → pool mérete lehet nagyobb, mert a szálak sokat várnak (pl. adatbázis, hálózat). - Virtual Thread esetén → nem kell pool méretén gondolkodni, a JVM kezeli.
Virtual Thread (Java 21+): A hagyományos (platform) szálak OS szálakhoz kötöttek, ezért drágák és limitáltak. A Virtual Thread ezzel szemben: - A JVM kezeli, nem az OS – ezért létrehozása szinte ingyenes. - Blokkoló hívás esetén (pl. adatbázis, hálózat) a JVM automatikusan felfüggeszti és felszabadítja az OS szálat, majd visszaadja ha az eredmény megérkezett. - Ezért akár több millió Virtual Thread is futhat egyszerre. - Nem pool-ozandó – minden feladathoz nyugodtan lehet újat létrehozni. - I/O-intenzív alkalmazásoknál (pl. REST API, adatbázis hívások) jelentős teljesítménynövekedést hozhat. - CPU-intenzív feladatoknál nem jelent előnyt, ott marad a platform thread pool.
5 – Mi az a Spring Security, és hogyan működik alap szinten?
A Spring Security egy keretrendszer, amely autentikációt (authentication – ki vagy?) és authorizációt (authorization – mit tehetsz?) biztosít Spring alkalmazásokban.
Hogyan működik? A Spring Security egy Filter Chain-re épül. Minden HTTP kérés végigmegy egy szűrőláncon, mielőtt elérné a controllert. Ha egy szűrő megakasztja a kérést (pl. nincs érvényes token), a kérés nem jut tovább.
Filter Chain főbb szűrői: - UsernamePasswordAuthenticationFilter – felhasználónév/jelszó alapú bejelentkezést kezel. - BasicAuthenticationFilter – HTTP Basic Auth fejlécet kezel. - BearerTokenAuthenticationFilter – JWT vagy OAuth2 token ellenőrzése. - ExceptionTranslationFilter – autentikációs és authorizációs hibákat kezel (pl. 401, 403 visszaadása).
SecurityContext: Sikeres autentikáció után a felhasználó adatai (principal) a SecurityContext-ben tárolódnak, amely ThreadLocal-ban él – tehát az aktuális szál számára érhető el a kérés teljes életciklusa alatt.
Autentikáció folyamata: 1. A kérés beérkezik a Filter Chain-be. 2. A megfelelő szűrő kiveszi a hitelesítési adatokat (pl. JWT token a headerből). 3. Az AuthenticationManager elvégzi az ellenőrzést. 4. Sikeres esetén a felhasználó adatai bekerülnek a SecurityContext-be. 5. A kérés továbbjut a controllerhez.
Authorizáció – kétféle módon:
1. URL szintű védelem – SecurityFilterChain bean: A konfigurációban meghatározható, hogy melyik endpoint milyen szerepkört igényel:
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
2. Metódus szintű védelem – @PreAuthorize: Finomabb vezérlésre alkalmas, Spring Expression Language (SpEL) alapú feltételeket is támogat: - @PreAuthorize("hasRole('ADMIN')") – szerepkör ellenőrzés. - @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") – összetettebb feltétel.
A kettő kombinálható – URL szinten durva védelem, metódus szinten finom vezérlés.
CSRF (Cross-Site Request Forgery): Olyan támadás, ahol egy rosszindulatú oldal a bejelentkezett felhasználó nevében küld kérést. A Spring Security alapból véd ellene. REST API-knál azonban általában kikapcsolják, mert JWT alapú stateless autentikációnál nem szükséges – a token nem kerül automatikusan a kérésbe, így a támadás nem kivitelezhető.
JWT (JSON Web Token): Egy önálló, aláírt token amely tartalmazza a felhasználó adatait (claims). Előnye, hogy szerveroldali session tárolás nélkül is működik – stateless autentikációt tesz lehetővé.
Fontos fogalmak: - Principal: Az aktuálisan bejelentkezett felhasználó. - GrantedAuthority: A felhasználóhoz rendelt jogosultság vagy szerepkör (pl. ROLE_ADMIN). - UserDetailsService: Egy interfész, amelyet implementálva megmondjuk a Springnek hogyan töltse be a felhasználót (pl. adatbázisból).
6 – Mi az a Kafka, és mikor érdemes használni?
A Kafka egy elosztott üzenetküldő rendszer (message broker), amely nagy mennyiségű adat valós idejű, megbízható továbbítására szolgál producerek és consumerek között.
Főbb fogalmak:
- Producer: Az az alkalmazás, amely üzeneteket küld a Kafkába.
- Consumer: Az az alkalmazás, amely üzeneteket olvas a Kafkából.
- Topic: Egy named channel, amelyre a producerek küldenek és a consumerek feliratkoznak. Olyan mint egy kategória vagy csatorna.
- Partition: A topic-ok partíciókra vannak osztva – ez teszi lehetővé a párhuzamos feldolgozást és a skálázhatóságot. Egy partíción belül az üzenetek sorrendje garantált.
- Offset: Minden üzenet kap egy sorszámot (offset) a partíción belül. A consumer nyilvántartja, melyik offsetig dolgozta fel az üzeneteket.
- Consumer Group: Több consumer alkothat egy csoportot. Egy partíciót egyszerre csak egy consumer olvashat a csoporton belül – ez biztosítja a terheléselosztást.
- Broker: Egy Kafka szerver. Több broker alkot egy Kafka clustert.
- Retention: A Kafka nem törli az üzeneteket azonnal a feldolgozás után – konfigurálható ideig megőrzi őket (pl. 7 nap). Ezért a consumerek visszamehetnek korábbi üzenetekre.
Kafka vs. hagyományos message queue (pl. RabbitMQ): - RabbitMQ-ban az üzenet feldolgozás után törlődik. - Kafkában az üzenet megmarad a retention idő végéig – több consumer group is feldolgozhatja ugyanazt az üzenetet egymástól függetlenül.
Kafka Connect: Adatok mozgatására szolgál Kafka és külső rendszerek között, kód írása nélkül, csak konfigurációval. - Source Connector – adatot hoz be a Kafkába külső rendszerből. - Sink Connector – adatot visz ki a Kafkából külső rendszerbe (pl. Elasticsearch, S3).
Példa: Egy PostgreSQL adatbázishoz kötsz egy Debezium Source Connector-t, amely figyeli az adatbázis változásait (INSERT, UPDATE, DELETE) és automatikusan tolja őket a Kafkába. Ezt hívják CDC-nek (Change Data Capture). Hasznos pl. audit log vagy microservice-ek közötti adatszinkronizáció esetén.
Kafka Streams: A Kafka beépített stream feldolgozó könyvtára. Közvetlenül a Kafka üzeneteken lehet valós idejű transzformációkat, szűréseket, aggregációkat végezni anélkül, hogy külön feldolgozó rendszert (pl. Apache Spark) kellene bevezetni.
Példa: Jönnek be rendelés események egy topic-ba, és te valós időben összeszámlálod hogy az elmúlt 5 percben mennyi rendelés érkezett – mindezt a Kafkán belül, egy külön szolgáltatás nélkül. Ezt hívják windowed aggregation-nek (ablakos aggregáció – időablakra vetített összesítés).
Mikor érdemes Kafkát használni: - Nagy mennyiségű esemény valós idejű feldolgozásakor (pl. kattintások, tranzakciók, logok). - Microservice-ek közötti aszinkron kommunikációhoz – a szolgáltatások lazán csatoltak maradnak. - Event sourcing vagy audit log esetén – az összes esemény visszajátszható. - Ha a consumer esetleg leáll és később folytatni kell a feldolgozást – a Kafka megőrzi az üzeneteket.
Mikor nem Kafka a legjobb választás: - Egyszerű request-response kommunikációhoz (arra REST vagy gRPC jobb). - Kis forgalmú rendszereknél, ahol egy egyszerűbb message queue is elegendő.
7 – Mi az az N+1 probléma JPA/Hibernate-ben, és hogyan lehet elkerülni?
Az N+1 probléma akkor fordul elő, amikor egy lekérdezés helyett N+1 SQL lekérdezés fut le – egy a lista lekéréséhez, és N darab az egyes elemekhez tartozó kapcsolódó adatok betöltéséhez.
Példa: Van egy Order entitás, amelynek van egy List<OrderItem> kapcsolata. Ha lekéred az összes rendelést, majd egyesével éred el az itemeket: - 1 SQL fut le az összes Order-re. - N SQL fut le minden egyes Order OrderItem-jeinek betöltéséhez. - 100 rendelésnél = 101 SQL lekérdezés.
Miért történik? Alapértelmezetten a @OneToMany és @ManyToMany kapcsolatok LAZY betöltésűek – a kapcsolódó adatok csak akkor töltődnek be, amikor hozzáférünk. Ha ezt egy ciklusban tesszük, minden iterációban új SQL fut.
Megoldások:
1. JOIN FETCH (JPQL): Egy lekérdezésben tölti be a kapcsolódó adatokat:
2. @EntityGraph: A repository metóduson jelölhető meg, hogy mely kapcsolatokat töltse be eagerly, anélkül hogy JPQL-t kellene írni:
3. Batch Fetching (@BatchSize): Nem egy lekérdezésbe vonja össze, hanem N helyett kötegekben (batch) tölti be a kapcsolódó adatokat:
4. DTO projekció: Csak a szükséges mezőket kérdezi le, nem tölt be teljes entitásokat – elkerüli a felesleges JOIN-okat.
Lazy vs Eager összefoglalva: - LAZY – csak akkor tölt be, amikor hozzáférünk. Alapértelmezett @OneToMany, @ManyToMany esetén. N+1 veszélye fennáll. - EAGER – azonnal betölti a kapcsolódó adatokat. Alapértelmezett @ManyToOne, @OneToOne esetén. Kerülendő @OneToMany-nál, mert mindig betölti a kapcsolódó adatokat, még ha nincs is rájuk szükség.