HaladóOlvasási idő: ~10 perc

Concurrency API

ExecutorService, Future, CompletableFuture és ForkJoinPool

A Java Concurrency API a task-alapú concurrency szabványos eszköztára. Lecseréli az ad-hoc threadgyártást explicit executorra, result handle-ökre, aszinkron kompozícióra és strukturált pool lifecycle-ra. Interjún ezzel azt mérik, hogy tudsz-e overloadról, backpressure-ről, cancellationről és pool ownershipről beszélni, nem csak osztályneveket felsorolni.

1. Definíció

Mi a Concurrency API?

A szabványos Java absztrakciók együttese, amelyekkel taskokat tudsz futtatni anélkül, hogy minden munkaegységhez külön raw threadet kezelnél.

Az alap elemek közé tartozik:

  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • Future
  • CompletableFuture
  • ForkJoinPool

Miért fontos ez?

A raw thread egyetlen helyre köti össze a következőket:

  • task submission
  • execution resource
  • lifecycle
  • shutdown szemantika

A Concurrency API ezeket szétválasztja.

Ez teszi lehetővé, hogy tudatosan gondolkodj:

  • capacityről
  • queueingről
  • rejectionről
  • cancellationről
  • result propagationről
  • ownershipről

Mit mond egy erős válasz?

Egy erős válasz elmagyarázza:

  • miért jobb többnyire executorokat használni raw thread helyett
  • miért architekturális döntés a pool sizing és a queueing
  • miért korlátozott a Future kompozíciós szempontból
  • miért jobb az orchestrationhöz a CompletableFuture
  • miért jó a ForkJoinPool CPU-bound felbontási munkára, de miért kockázatos blocking workloadra

2. Alapfogalmak

2.1 `Executor` és `ExecutorService`

Az Executor a minimális absztrakció:

  • futtasd ezt a taskot valahogy

Az ExecutorService ehhez hozzáadja:

  • a submission API-kat
  • a shutdown API-kat
  • a Future eredményeket
  • a bulk task metódusokat

A fontos design kérdés nem csak az, hogy melyik factory metódust hívod.

Hanem az is, hogy:

  • hány thread van?
  • milyen queue típust használsz?
  • mi történik overload alatt?

2.1.1 Kifejezések és szerződések, amelyeket itt érdemes kimondani

Ezek a fontos contract szavak ebben a témában:

  • Executor — minimális task execution absztrakció
  • ExecutorService — lifecycle-lal és result handlinggel rendelkező executor
  • ThreadPoolExecutor — konfigurálható pool implementáció
  • queue capacity — backlog határ, ahol megjelenik a nyomás vagy a rejection
  • rejection policy — mi történik, ha a pool már nem tud több munkát fogadni
  • Future — pending result handle
  • cancellation — kérés a futó task megszakítására vagy az eredmény eldobására
  • CompletableFuture — kompozícionálható aszinkron stage API
  • ForkJoinPool — work-stealing pool finomszemcsés taskokhoz
  • work stealing — idle workerek más workerektől lopnak taskot
  • common pool — megosztott alapértelmezett ForkJoinPool
  • shutdown() — graceful, több taskot már nem fogadó leállítás
  • shutdownNow() — erősebb, interruption-orientált leállítási kísérlet

2.2 `Future` versus `CompletableFuture`

A Future akkor hasznos, ha kell:

  • egy pending eredmény
  • blokkoló várakozás get()-tel
  • cancellation lehetőség

A gyenge pontja a kompozíció.

Sok Future összefűzése gyakran egymásba ágyazott blokkolást vagy kényelmetlen orchestrationt eredményez.

A CompletableFuture függő stage-eket modellez.

Lehetővé teszi, hogy:

  • eredményt transzformálj
  • eredményeket kombinálj
  • hibából visszaállj
  • timeoutot alkalmazz
  • aszinkron pipeline-t építs

2.3 Pool viselkedés és overload

A fix méretű pool unbounded queue-val kordában tartja a threadek számát.

De az overloadot elrejtheti így:

  • növekvő latency
  • növekvő queue memóriaigény
  • késleltetett hiba

A cached vagy elastic pool csökkenti a queueinget.

Viszont burst alatt túl sok threadet hozhat létre.

Ezért a queue policy nem implementációs részlet.

Ez a service contract része.

2.4 `ForkJoinPool`

A ForkJoinPool rekurzív felbontásra és finomszemcsés CPU-bound feladatokra optimalizált.

A workerek saját deque-et tartanak fenn.

Az idle worker másoktól tud taskot lopni.

Ez hatékony divide-and-conquer workloadnál.

Blocking I/O-heavy munkára többnyire rossz alapértelmezés, hacsak nem érted mélyen a trade-offokat.

3. Gyakorlati használat

Explicit pool ownership

Ha a komponens létrehoz egy poolt, akkor rendszerint a shutdown is az ő felelőssége.

Ha a pool megosztott infrastruktúra, a komponens ne zárja le csak úgy.

Ennek az ownership boundarynek kódban is egyértelműnek kell lennie.

Production-barát alapbeállítások

Amikor üzemeltetési viselkedés számít, érdemes explicit ThreadPoolExecutor konfigurációt használni.

Így meghatározhatod:

  • a thread countot
  • a queue típusát és méretét
  • a thread naminget
  • a rejection policyt
  • a keep-alive viselkedést

A threadnév és a rejection policy nem opcionális díszítés.

Ezek az observability és overload behavior részei.

`CompletableFuture` fegyelem

A CompletableFuture erős eszköz.

De könnyű túlhasználni.

Minden aszinkron stage-nek legyen értelmes execution contextje.

Ha nem adsz meg executort, az async stage-ek gyakran a common poolt használják.

Ez könnyen összekever különböző workloadokat.

Graceful shutdown

A helyes leállítás tipikusan azt jelenti, hogy:

  • nem fogadsz több új munkát
  • megvárod a folyamatban levő taskokat
  • csak szükség esetén eszkalálsz

A vakon meghívott shutdownNow() félbehagyott munkát eredményezhet.

4. Kód példák

1. példa: explicit thread pool

ThreadFactory factory = runnable -> {
    Thread thread = new Thread(runnable);
    thread.setName("pricing-worker-" + THREAD_ID.incrementAndGet());
    return thread;
};

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    8,
    16,
    60,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500),
    factory,
    new ThreadPoolExecutor.CallerRunsPolicy()
);

Kulcspontok:

  • explicit boundok
  • elnevezett workerek
  • explicit queue
  • explicit rejection policy

2. példa: `Future` eredménykezelés

Future<Price> future = executor.submit(() -> pricingService.calculate(order));
Price price = future.get(200, TimeUnit.MILLISECONDS);

Kulcspontok:

  • a Future egyetlen pending eredmény fogantyúja
  • a get() blokkoló határpont
  • a timeout gyakran a caller contract része

3. példa: `CompletableFuture` kompozíció

CompletableFuture<Customer> customerFuture = CompletableFuture
    .supplyAsync(() -> customerClient.fetch(customerId), ioExecutor);

CompletableFuture<Balance> balanceFuture = CompletableFuture
    .supplyAsync(() -> accountClient.fetchBalance(customerId), ioExecutor);

CompletableFuture<Summary> summaryFuture = customerFuture.thenCombine(
    balanceFuture,
    Summary::new
).orTimeout(300, TimeUnit.MILLISECONDS)
 .exceptionally(ex -> Summary.fallback(customerId));

Summary summary = summaryFuture.join();

Kulcspontok:

  • a független hívások egyszerre futhatnak
  • a kombináció explicit
  • a timeout és a fallback a pipeline része

4. példa: graceful shutdown

executor.shutdown();
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
    executor.shutdownNow();
}

Kulcspontok:

  • először graceful leállítás
  • utána eszkaláció, ha kell
  • a lifecycle explicit

5. Trade-offok

Döntés Előny Költség vagy kockázat
ExecutorService Jobb lifecycle és pooling, mint a raw thread Sizing és ownership döntések kellenek
Unbounded queue Nem nő kontroll nélkül a threadszám Elrejtheti az overloadot latency és memória formájában
Bounded queue Láthatóvá teszi a nyomást Kell hozzá rejection stratégia
Future Egyszerű pending result handle Gyenge kompozíciós modell
CompletableFuture Erős orchestration és recovery lehetőségek Könnyű túlkomplikálni a flow-t
ForkJoinPool Hatékony finomszemcsés CPU-munkára Kockázatos blocking workloadnál

Gyakorlati trade-off elemzés

A Concurrency API kontrollt ad.

De ezzel együtt döntéseket is kikényszerít.

Ezek a döntések production viselkedéssé válnak.

Ezért a jó válasz nem az, hogy:

  • „ismerem az ExecutorService-t”

Hanem az, hogy:

  • „értem, milyen queueing, rejection és shutdown viselkedést választ a szolgáltatásom”

6. Gyakori hibák

1. hiba: convenience factory használata a pool viselkedés megértése nélkül

A factory metódusok kényelmesek.

Az üzemeltetési viselkedésük viszont nem mindig az, amit szeretnél.

Helyes megközelítés:

  • értsd a threadszámot, queueinget és rejection policyt

2. hiba: a saját executor shutdownjának elfelejtése

Ez threadszivárgást és váratlan életben maradó folyamatokat okoz.

Helyes megközelítés:

  • tedd explicitte az ownershipet és graceful módon állítsd le

3. hiba: a common pool blokkolása felelőtlenül

A megosztott pool éheztethet más, nem kapcsolódó munkákat.

Helyes megközelítés:

  • blocking vagy üzletkritikus workloadhoz használj dedikált executort

4. hiba: úgy kezelni a `Future`-t, mintha jól kompozícionálható lenne

Nem az.

Helyes megközelítés:

  • orchestration esetén inkább CompletableFuture

5. hiba: aszinkron stage-ek szétszórása mindenhová

A tiszta határok nélküli aszinkronitás megnehezíti a tracinget és a hibakezelést.

Helyes megközelítés:

  • ott használj async kompozíciót, ahol valódi concurrency vagy latency előny van

6. hiba: overload viselkedés figyelmen kívül hagyása

A stressz alatti poolnak is kell policy.

Helyes megközelítés:

  • döntsd el, hogy queue-olni, rejectelni vagy visszanyomást adni akarsz-e a callernek

7. Mélymerülés

7.1 A queueing architektúra

Egy unbounded queue elsőre biztonságosnak tűnik, mert a taskokat elfogadja.

De a rejtett overload gyakran rosszabb, mint a látható rejection.

Ez a következőket eredményezheti:

  • növekvő latency
  • elöregedő munka
  • memória-nyomás
  • rossz tail viselkedés

7.2 Ownership és shutdown

Egy executor infrastruktúra-erőforrás.

Ezért a lifecycle-jának választ kell adnia arra, hogy:

  • ki hozza létre?
  • ki állítja le?
  • mi történik a queued workkel?
  • mi történik alkalmazásleálláskor?

Ha ezekre nincs világos válasz, abból incidens lesz.

7.3 `CompletableFuture` mint orchestration eszköz

A CompletableFuture akkor a legerősebb, amikor függőségi gráfokat fejez ki.

Gyengébb akkor, amikor csak „async mindenhová” módon használják.

Az érett válasz kiemeli:

  • a stage kompozíciót
  • a timeout elhelyezését
  • a fallback határokat
  • az executor választást

7.4 `ForkJoinPool` realizmus

A work stealing kiváló CPU-bound rekurzív workloadokra.

Nem varázslat.

Ha a taskok sokat blokkolnak, a throughput és fairness sérülhet.

Ezért gyenge válasz az, hogy „használjuk a common poolt mindenre”.

8. Interjúkérdések

1. Miért jobb az `ExecutorService`, mint a raw thread?

Mert szétválasztja a task submissiont az execution resource menedzsmenttől, és lifecycle kontrollt ad.

2. Mit reprezentál a `Future`?

Egy pending eredmény fogantyúját.

3. Miért jobb gyakran orchestrationre a `CompletableFuture`?

Mert sokkal tisztábban kompozícionálhatóak a függő aszinkron stage-ek.

4. Miért fontos a queue választás?

Mert a queueing megváltoztatja az overload viselkedést, a latencyt és a memóriahasználatot.

5. Mit csinál a rejection policy?

Meghatározza, mi történik, amikor az executor már nem tud több munkát elfogadni.

6. Miért fontos a thread naming poolokban?

Mert jobb diagnosztikát és observabilityt ad.

7. Mikor jó a `ForkJoinPool`?

Finomszemcsés CPU-bound rekurzív munkára.

8. Miért veszélyes a common pool blokkolása?

Mert a megosztott execution resource-on más taskok is éhezhetnek.

9. Mi egy jó shutdown sorrend saját executor esetén?

shutdown(), várakozás, majd szükség esetén eszkaláció.

10. Mi a senior szintű tanulság?

A pool viselkedése a rendszer szerződésének része, nem puszta implementációs részlet.

9. Szószedet

Fogalom Jelentés
Executor Minimális task execution absztrakció
ExecutorService Lifecycle-lal és result handlinggel rendelkező executor
ThreadPoolExecutor Konfigurálható executor implementáció
Future Pending eredmény fogantyúja
CompletableFuture Kompozícionálható aszinkron stage API
rejection policy Viselkedés, ha több munka már nem fogadható
bounded queue Explicit kapacitáslimites queue
common pool Megosztott alapértelmezett ForkJoinPool
work stealing Az idle worker más workertől lop taskot
graceful shutdown Új munka tiltása, miközben a folyamatban levő taskok befejeződnek

10. Gyorsreferencia

  • általános task dispatchre többnyire executor kell, nem raw thread
  • a pool sizing és queueing architekturális döntés
  • nevezd el a worker threadeket
  • tedd explicitté a rejection viselkedést
  • a Future jó egy pending eredményhez
  • a CompletableFuture jó orchestrationre és recoveryre
  • ne blokkolj felelőtlenül a common poolon
  • a saját executorokat explicit módon állítsd le
  • overloadnál bounded erőforrásokkal gondolkodj
  • interjún nevezd meg a queueing, rejection, ownership és shutdown trade-offokat

🎮 Játékok

10 kérdés