Kihagyás

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:

  1. 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).
  2. Old Generation – Major GC:

    • A hosszú életű objektumok helye.
    • Ritkábban fut, de tovább tart.
  3. 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:

SELECT o FROM Order o JOIN FETCH o.items

2. @EntityGraph: A repository metóduson jelölhető meg, hogy mely kapcsolatokat töltse be eagerly, anélkül hogy JPQL-t kellene írni:

@EntityGraph(attributePaths = {"items"})
List<Order> findAll();

3. Batch Fetching (@BatchSize): Nem egy lekérdezésbe vonja össze, hanem N helyett kötegekben (batch) tölti be a kapcsolódó adatokat:

@BatchSize(size = 20)
private List<OrderItem> items;

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.